round 2 android e2e fixes: bundled media asset, report dialog deps, flow guards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -153,16 +153,6 @@ 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"
|
||||
|
||||
@@ -44,6 +44,12 @@ appId: xyz.blueskyweb.app
|
||||
id: "e2eRefreshHome"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing. Tapping replyBtn right
|
||||
# after the previous publish can race the closing composer on Android.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
@@ -51,6 +57,11 @@ appId: xyz.blueskyweb.app
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply with an image"
|
||||
- tapOn:
|
||||
id: "openMediaBtn"
|
||||
@@ -63,6 +74,11 @@ appId: xyz.blueskyweb.app
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
@@ -29,86 +29,81 @@ appId: xyz.blueskyweb.app
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Set alice-favs first
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
# 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).
|
||||
# Reordering feeds is driven by a drag on the feed-drag-handle. Maestro cannot
|
||||
# activate the RNGH Pan gesture from a synthetic swipe on Android (proven
|
||||
# twice - coordinate swipes never register the pan), so the reorder
|
||||
# verification below runs on iOS only. If Android drag coverage is needed,
|
||||
# revisit with the SavedFeedsA11y move buttons rather than a swipe.
|
||||
- runFlow:
|
||||
when:
|
||||
platform: iOS
|
||||
commands:
|
||||
# Set alice-favs first
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- 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"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "saveChangesBtn"
|
||||
enabled: true
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "Following"
|
||||
|
||||
# Set following first
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- runFlow:
|
||||
when:
|
||||
platform: iOS
|
||||
commands:
|
||||
# Set following first
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- swipe:
|
||||
label: "Drag feed down"
|
||||
from:
|
||||
id: "feed-drag-handle"
|
||||
direction: "DOWN"
|
||||
duration: 1000
|
||||
- assertVisible:
|
||||
id: "saveChangesBtn"
|
||||
enabled: true
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# On Android, the reorder path above is skipped. Smoke-test that the feeds
|
||||
# edit screen opens and the pinned feeds render, then return to a valid state.
|
||||
- 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"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- assertVisible: "Following"
|
||||
- assertVisible: "alice-favs"
|
||||
- tapOn: "Go back"
|
||||
|
||||
# Remove following
|
||||
- tapOn: "Open drawer menu"
|
||||
|
||||
@@ -15,8 +15,13 @@ appId: xyz.blueskyweb.app
|
||||
- tapOn:
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Open avatar creator"
|
||||
- extendedWaitUntil:
|
||||
visible: "Select an emoji"
|
||||
# The avatar-creator bottom sheet opens as a sliver on the short E2E emulator
|
||||
# (720x1600), so the picker content sits below the fold. Scroll it into view
|
||||
# instead of just waiting. No-op on iOS, where it is already visible.
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
text: "Select an emoji"
|
||||
direction: DOWN
|
||||
timeout: 15000
|
||||
- tapOn: "Select the zap emoji as your avatar"
|
||||
- tapOn:
|
||||
|
||||
@@ -64,7 +64,10 @@ appId: xyz.blueskyweb.app
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible: "alice.test"
|
||||
# The display-name node renders the handle as a Text with a nested badge View
|
||||
# once the display name is cleared, so the a11y text is not the bare handle
|
||||
# string on Android. Match it as a substring instead.
|
||||
- assertVisible: ".*alice\\.test.*"
|
||||
- assertNotVisible: "One cool hacker"
|
||||
|
||||
# Set avi and banner via the edit profile modal
|
||||
|
||||
@@ -20,6 +20,12 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "Test thread"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
# Wait for the composer to close and the home feed to settle before signing
|
||||
# out. Without a settle guard the next action can race the closing composer.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composeFAB"
|
||||
timeout: 10000
|
||||
|
||||
# Login, reply to the thread, and log out
|
||||
- tapOn:
|
||||
@@ -31,9 +37,19 @@ appId: xyz.blueskyweb.app
|
||||
id: "viewHeaderHomeFeedPrefsBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
# Wait for the composer to close before signing out.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composeFAB"
|
||||
timeout: 10000
|
||||
|
||||
# Login, confirm notification exists, mute thread, and log out
|
||||
- tapOn:
|
||||
@@ -67,14 +83,36 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
# Wait for the composer to close and the thread reply button to return before
|
||||
# opening the composer again. Previously this raced and the second replyBtn tap
|
||||
# landed before the composer was ready, so the composer never opened.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "replyBtn"
|
||||
timeout: 10000
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
# Wait for the composer to fully open before typing.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "composerPublishBtn"
|
||||
timeout: 10000
|
||||
- inputText: "Reply 3"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
# Wait for the composer to close and the thread to settle before signing out.
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "replyBtn"
|
||||
timeout: 10000
|
||||
|
||||
# Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~
|
||||
# Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
"expo": "54.0.34",
|
||||
"expo-age-range": "0.2.18",
|
||||
"expo-application": "~7.0.8",
|
||||
"expo-asset": "~12.0.13",
|
||||
"expo-blur": "~15.0.8",
|
||||
"expo-build-properties": "~1.0.10",
|
||||
"expo-camera": "~17.0.10",
|
||||
|
||||
Generated
+3
@@ -436,6 +436,9 @@ importers:
|
||||
expo-application:
|
||||
specifier: ~7.0.8
|
||||
version: 7.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
|
||||
expo-asset:
|
||||
specifier: ~12.0.13
|
||||
version: 12.0.13(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||
expo-blur:
|
||||
specifier: ~15.0.8
|
||||
version: 15.0.8(expo@54.0.34(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||
|
||||
@@ -249,7 +249,23 @@ function Inner(props: ReportDialogProps) {
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}, [logger, submitReport, props, state, ax, l])
|
||||
/*
|
||||
* Depend on the specific props members used, not the whole props object.
|
||||
* props is a new object every render, so depending on it recreates this
|
||||
* callback each render, which on Android tears down the pending
|
||||
* setTimeout(close) before it fires and the dialog never closes on the
|
||||
* fast submit path (APP-1999).
|
||||
*/
|
||||
}, [
|
||||
logger,
|
||||
submitReport,
|
||||
props.subject,
|
||||
props.control,
|
||||
props.onAfterSubmit,
|
||||
state,
|
||||
ax,
|
||||
l,
|
||||
])
|
||||
|
||||
useCallOnce(() => {
|
||||
ax.metric('reportDialog:open', {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {Asset} from 'expo-asset'
|
||||
import {
|
||||
documentDirectory,
|
||||
getInfoAsync,
|
||||
@@ -13,17 +14,6 @@ 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()
|
||||
@@ -57,25 +47,35 @@ async function getFile() {
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* The Android emulator can't reach the iOS simulator's sample photo library,
|
||||
* so we load a jpg bundled with the app instead. It is bundled via require()
|
||||
* (resolved by Metro), so it survives `pm clear`, which Maestro's clearState
|
||||
* runs at the start of every flow. An adb-seeded file in app-scoped external
|
||||
* storage does not survive: pm clear wipes that directory each flow, so the
|
||||
* seeded file is gone before the picker mock ever reads it.
|
||||
*/
|
||||
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 asset = Asset.fromModule(
|
||||
require('../../../assets/images/welcome-modal-bg.jpg'),
|
||||
)
|
||||
await asset.downloadAsync()
|
||||
|
||||
const fileInfo = await getInfoAsync(file)
|
||||
const path = asset.localUri!
|
||||
const fileInfo = await getInfoAsync(path)
|
||||
|
||||
if (!fileInfo.exists) {
|
||||
throw new Error('Failed to get file info')
|
||||
}
|
||||
|
||||
/*
|
||||
* Dimensions of the seeded asset (assets/images/welcome-modal-bg.jpg). Only
|
||||
* Dimensions of the bundled 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,
|
||||
path,
|
||||
mime: 'image/jpeg',
|
||||
size: fileInfo.size,
|
||||
width: 1432,
|
||||
|
||||
Reference in New Issue
Block a user