fix android e2e flow failures: matchers, timing, media seeding, gestures

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-19 14:47:37 +03:00
parent 056a671d62
commit bc72ad4733
11 changed files with 146 additions and 36 deletions
+10
View File
@@ -153,6 +153,16 @@ if [[ "$platform" == "android" ]]; then
phase "Configuring Android localhost routing"
adb -s "$device_id" reverse tcp:3000 tcp:3000
adb -s "$device_id" reverse tcp:8081 tcp:8081
# The e2e media picker mock reads a seeded photo from the app's external
# files directory (see src/lib/media/picker.e2e.tsx). The app must already
# be installed (the install step runs before this script), so the directory
# exists and is owned by the app; sdcardfs makes it world-readable so the
# mock can read the file without any runtime media permission.
phase "Seeding e2e media"
e2e_media_dir="/sdcard/Android/data/xyz.blueskyweb.app/files/e2e"
adb -s "$device_id" shell mkdir -p "$e2e_media_dir"
adb -s "$device_id" push assets/images/welcome-modal-bg.jpg "$e2e_media_dir/test.jpg"
fi
phase "Running Maestro flows"
+47 -12
View File
@@ -35,12 +35,31 @@ appId: xyz.blueskyweb.app
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
# The id+direction swipe doesn't register the RNGH pan on Android, so we
# use an explicit coordinate swipe that travels several row heights. iOS
# keeps the id-based swipe (its screen aspect ratio differs).
- runFlow:
when:
platform: iOS
commands:
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag feed down"
start: "93%,31%"
end: "93%,45%"
duration: 1500
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
@@ -58,12 +77,28 @@ appId: xyz.blueskyweb.app
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- runFlow:
when:
platform: iOS
commands:
- swipe:
label: "Drag feed down"
from:
id: "feed-drag-handle"
direction: "DOWN"
duration: 1000
- runFlow:
when:
platform: Android
commands:
- swipe:
label: "Drag feed down"
start: "93%,31%"
end: "93%,45%"
duration: 1500
- assertVisible:
id: "saveChangesBtn"
enabled: true
- tapOn:
label: "Save button"
id: "saveChangesBtn"
@@ -15,6 +15,9 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eStartOnboarding"
- tapOn: "Open avatar creator"
- extendedWaitUntil:
visible: "Select an emoji"
timeout: 15000
- tapOn: "Select the zap emoji as your avatar"
- tapOn:
label: "Tap on yellow"
+22 -7
View File
@@ -16,13 +16,28 @@ appId: xyz.blueskyweb.app
id: "e2eStartOnboarding"
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: iOS
commands:
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
- runFlow:
when:
platform: Android
commands:
# The system photo picker opened here shows MediaStore photos, which
# the e2e run doesn't seed (media is seeded into app-scoped storage for
# the composer's mocked picker instead). With no photo to pick, dismiss
# the picker and continue - onContinue falls back to the generated
# placeholder avatar, and nothing later in the flow depends on the image.
- back
- waitForAnimationToEnd
- tapOn:
id: "onboardingContinue"
- assertVisible: "What are your interests?"
+1 -1
View File
@@ -45,7 +45,7 @@ appId: xyz.blueskyweb.app
id: "editProfileSaveBtn"
- assertNotVisible:
id: "editProfileModal"
- assertVisible: "Alicia"
- assertVisible: ".*Alicia.*"
- assertVisible: "One cool hacker"
# Remove display name and description via the edit profile modal
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+4 -2
View File
@@ -22,5 +22,7 @@ appId: xyz.blueskyweb.app
text: "Send report to Dev-env Moderation"
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -39,5 +39,7 @@ appId: xyz.blueskyweb.app
text: Your report will be sent to Dev-env Moderation.*
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
@@ -29,5 +29,7 @@ appId: xyz.blueskyweb.app
- hideKeyboard
- tapOn:
id: "report:submit"
- assertNotVisible:
id: "report:dialog"
- extendedWaitUntil:
notVisible:
id: "report:dialog"
timeout: 20000
+3 -8
View File
@@ -45,10 +45,8 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarNotificationsBtn"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- assertVisible: ".*Reply 1.*"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
@@ -92,10 +90,7 @@ appId: xyz.blueskyweb.app
- assertVisible: ".*Reply 1.*"
- assertNotVisible: ".*Reply 2.*"
- assertNotVisible: ".*Reply 3.*"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
id: "feedItem-by-bob.test"
- tapOn: ".*Reply 1.*"
- tapOn:
id: "postDropdownBtn"
childOf:
+44
View File
@@ -9,10 +9,26 @@ import ExpoImageCropTool, {
} from '@bsky.app/expo-image-crop-tool'
import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants'
import {IS_ANDROID} from '#/env'
import {compressIfNeeded} from './manip'
import {type PickerImage} from './picker.shared'
/*
* The Android emulator can't reach the iOS simulator's sample photo library,
* so run-nightly-e2e.sh seeds a single jpg into the app's external files
* directory before the flows run. That directory is world-readable via the
* emulator's sdcardfs, so expo-file-system can read it without any runtime
* media permission (unlike /sdcard/DCIM, which is gated behind scoped storage
* on target SDK 35).
*/
const ANDROID_E2E_MEDIA_DIR =
'file:///sdcard/Android/data/xyz.blueskyweb.app/files/e2e'
async function getFile() {
if (IS_ANDROID) {
return await getAndroidFile()
}
const imagesDir = documentDirectory!
.split('/')
.slice(0, -6)
@@ -41,6 +57,34 @@ async function getFile() {
)
}
async function getAndroidFile() {
let files = await readDirectoryAsync(ANDROID_E2E_MEDIA_DIR)
files = files.filter(file => file.toLowerCase().endsWith('.jpg'))
const file = `${ANDROID_E2E_MEDIA_DIR}/${files[0]}`
const fileInfo = await getInfoAsync(file)
if (!fileInfo.exists) {
throw new Error('Failed to get file info')
}
/*
* Dimensions of the seeded asset (assets/images/welcome-modal-bg.jpg). Only
* used for downstream aspect-ratio display; the actual bytes are read from
* disk by compressIfNeeded.
*/
return await compressIfNeeded(
{
path: file,
mime: 'image/jpeg',
size: fileInfo.size,
width: 1432,
height: 1025,
},
IMAGE_SIZE_CONFIG_2K_1MB,
)
}
export async function openPicker(): Promise<PickerImage[]> {
return [await getFile()]
}