diff --git a/.github/scripts/run-nightly-e2e.sh b/.github/scripts/run-nightly-e2e.sh index fe4fb7e355..0a2c57252f 100755 --- a/.github/scripts/run-nightly-e2e.sh +++ b/.github/scripts/run-nightly-e2e.sh @@ -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" diff --git a/__e2e__/flows/feed-reorder.yml b/__e2e__/flows/feed-reorder.yml index 42abc3295e..ac00eefc41 100644 --- a/__e2e__/flows/feed-reorder.yml +++ b/__e2e__/flows/feed-reorder.yml @@ -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" diff --git a/__e2e__/flows/onboarding-avatar-creator.yml b/__e2e__/flows/onboarding-avatar-creator.yml index 2204abb9df..aa0e1d794a 100644 --- a/__e2e__/flows/onboarding-avatar-creator.yml +++ b/__e2e__/flows/onboarding-avatar-creator.yml @@ -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" diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml index a5f4217455..4c0ffdd48f 100644 --- a/__e2e__/flows/onboarding.yml +++ b/__e2e__/flows/onboarding.yml @@ -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?" diff --git a/__e2e__/flows/profile-screen-edit.yml b/__e2e__/flows/profile-screen-edit.yml index f60ca01b42..2161e98469 100644 --- a/__e2e__/flows/profile-screen-edit.yml +++ b/__e2e__/flows/profile-screen-edit.yml @@ -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 diff --git a/__e2e__/flows/report-dialog/account.default.yml b/__e2e__/flows/report-dialog/account.default.yml index 372fc31c7e..c5c5169cdc 100644 --- a/__e2e__/flows/report-dialog/account.default.yml +++ b/__e2e__/flows/report-dialog/account.default.yml @@ -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 diff --git a/__e2e__/flows/report-dialog/post.default.yml b/__e2e__/flows/report-dialog/post.default.yml index be3ac6b68a..212accc254 100644 --- a/__e2e__/flows/report-dialog/post.default.yml +++ b/__e2e__/flows/report-dialog/post.default.yml @@ -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 diff --git a/__e2e__/flows/report-dialog/post.edit-reason.yml b/__e2e__/flows/report-dialog/post.edit-reason.yml index eec5794c4f..a44e7f97d7 100644 --- a/__e2e__/flows/report-dialog/post.edit-reason.yml +++ b/__e2e__/flows/report-dialog/post.edit-reason.yml @@ -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 diff --git a/__e2e__/flows/report-dialog/post.reason-other.yml b/__e2e__/flows/report-dialog/post.reason-other.yml index e1065ece4d..78f5a195ec 100644 --- a/__e2e__/flows/report-dialog/post.reason-other.yml +++ b/__e2e__/flows/report-dialog/post.reason-other.yml @@ -29,5 +29,7 @@ appId: xyz.blueskyweb.app - hideKeyboard - tapOn: id: "report:submit" -- assertNotVisible: - id: "report:dialog" +- extendedWaitUntil: + notVisible: + id: "report:dialog" + timeout: 20000 diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml index 2724833feb..ac5bf57063 100644 --- a/__e2e__/flows/thread-muting.yml +++ b/__e2e__/flows/thread-muting.yml @@ -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: diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index 7aaa69c47d..af84e29c2b 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -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 { return [await getFile()] }