Custom scrolling for screen fragments
Scroll to elements inside fragments with custom swipe logic when scrollUntilVisible fails.
Maestro’s native scrollUntilVisible is a powerful tool, but it works in a specific way:
- Checks for the element defined by the selector.
- Swipe up from the center of the screen.
- Return to step 1.
While this works for most full-screen lists, it fails in apps where only a specific part of the screen is scrollable, such as a bottom sheet, a small side-scrolling widget, or a fragmented UI where the top half is static. If the center of your screen isn't scrollable, the native command will simply tap a static element repeatedly.
This recipe shows you how to build a scrolling logic that swipes exactly where you need it to.
Credits
This issue was first reported by the developers at Amicara. Their team was instrumental in helping create this solution to overcome difficulties scrolling through complex, fragmented layouts.
The workflow
To bypass the center-swipe limitation, we recreate the scrolling logic manually using a loop. Instead of letting Maestro decide where to swipe, we define specific screen coordinates to ensure the swipe happens within the scrollable container.
1. Reusable scroll Flow
This subflow (e.g., scroll_fragment.yaml) receives the TARGET_ID from the parent Flow. It continues to swipe in a restricted area of the screen until the target element is discovered.
# scroll_fragment.yaml
# Custom scrolling for targeted screen areas
# Required Env: TARGET_ID (The resource id of the element to find)
# Step 1: Initialize the state
# Use a flag to track if we've found our target yet
- evalScript: ${output.foundElement = 0}
# Step 2: Immediate check
# Before we start moving, let's see if the element is already there
- runFlow:
when:
visible:
id: "${TARGET_ID}"
commands:
- evalScript: ${output.foundElement = 1}
# Step 3: The targeted loop
# We continue swiping in the bottom region until foundElement becomes 1
- repeat:
while:
true: ${output.foundElement == 0}
commands:
# We swipe from 90% down the screen to 75% down the screen.
# This keeps the interaction strictly within the bottom fragment.
- swipe:
start: 50%, 90%
end: 50%, 75%
# Check again after the movement
# If the element is visible, we stop the loop
- runFlow:
when:
visible:
id: "${TARGET_ID}"
commands:
- evalScript: ${output.foundElement = 1}This implementation gives you full control over the scroll mechanics. In the swipe command, using percentages such as 90% and 75% ensures consistent behavior across different device sizes. By targeting the lower portion of the screen, you avoid accidentally interacting with static headers or non-scrollable hero sections. Additionally, the search flag (${output.foundElement}) acts as a bridge between the runFlow step, which checks visibility, and the repeat loop, which controls the scrolling action.
Adapt it to your needs
If the static area differs in your app, adjust the swipe command configuration to suit your needs.
Preventing infinite swiping
While not shown in this basic snippet, you can easily add a counter to the repeat loop to throw an error if the element isn't found after 10 or 20 swipes, preventing your test from hanging indefinitely.
2. Implementation
To use this helper in your main Flow, call it with runFlow and pass the TARGET_ID of the element you need to find as an environment variable.
# main_test.yaml
- tapOn: "Open Settings"
# Find a specific setting at the bottom of a scrollable fragment
- runFlow:
file: ../utils/scroll_fragment.yaml
env:
TARGET_ID: "logout_button"
# Once the subflow finishes, the element is confirmed visible
- tapOn:
id: "logout_button"Related content
Explore the following documentation pages if some of the concepts used in this recipe are not clear for you:
