about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-08-29 18:15:05 +0300
committerGitHub <noreply@github.com>2025-08-29 08:15:05 -0700
commitd6dc52b6eadade991846c61e748d09a6f2b0ef78 (patch)
tree4f984fe151c6b5f58b3e245ed3a4721ecbb3af9c
parent541502c7eeaf54aed3468136c89a2496df16850c (diff)
downloadvoidsky-d6dc52b6eadade991846c61e748d09a6f2b0ef78.tar.zst
Improve e2e tests (#8927)
* get e2e image picker working

* verify create account actually reaches onboarding

* wait for image to actually be attached before posting

* wait until login finishes before moving on

* sign out before switch accounts then wait until logged in

* disable onboarding experiments in e2e

* add testId to handle availability checkmark

* fix too long username

* update thread muting test to reflect current behaviour

* hackfix for the british english translation

* unflake the onboarding tests

* fix curate list flow

* admit defeat on the most list one
-rw-r--r--__e2e__/flows/composer-self-label.yml12
-rw-r--r--__e2e__/flows/composer.yml23
-rw-r--r--__e2e__/flows/create-account.yml5
-rw-r--r--__e2e__/flows/curate-lists.yml12
-rw-r--r--__e2e__/flows/mod-lists.yml20
-rw-r--r--__e2e__/flows/onboarding-avatar-creator.yml16
-rw-r--r--__e2e__/flows/onboarding.yml16
-rw-r--r--__e2e__/flows/post-report-flow.yml5
-rw-r--r--__e2e__/flows/profile-screen.yml8
-rw-r--r--__e2e__/flows/search-screen.yml10
-rw-r--r--__e2e__/flows/shared-prefs.yml6
-rw-r--r--__e2e__/flows/thread-muting.yml47
-rw-r--r--__e2e__/mock-server.ts24
-rw-r--r--__e2e__/setupServer.js4
-rw-r--r--src/lib/media/picker.e2e.tsx17
-rw-r--r--src/lib/media/picker.shared.ts22
-rw-r--r--src/lib/media/picker.tsx6
-rw-r--r--src/lib/media/picker.web.tsx2
-rw-r--r--src/screens/Onboarding/StepFinished.tsx1
-rw-r--r--src/screens/Onboarding/StepInterests/index.tsx1
-rw-r--r--src/screens/Onboarding/StepProfile/index.tsx2
-rw-r--r--src/screens/Onboarding/index.tsx6
-rw-r--r--src/screens/Signup/StepHandle/index.tsx5
-rw-r--r--src/view/com/composer/SelectMediaButton.tsx22
24 files changed, 200 insertions, 92 deletions
diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml
index b91ff7507..4539e1e04 100644
--- a/__e2e__/flows/composer-self-label.yml
+++ b/__e2e__/flows/composer-self-label.yml
@@ -3,20 +3,26 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: ?users
+      SERVER_PATH: ?users
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 
 # Post an image with the porn label
 - assertVisible:
-      id: "composeFAB"
+    id: "composeFAB"
 - tapOn:
     id: "composeFAB"
 - inputText: "Post with an image"
 - tapOn:
-    id: "openGalleryBtn"
+    id: "openMediaBtn"
+- extendedWaitUntil:
+    visible:
+      id: "selectedPhotosView"
 - tapOn: "Content warnings"
 - tapOn: "Porn"
 - tapOn:
diff --git a/__e2e__/flows/composer.yml b/__e2e__/flows/composer.yml
index d6cf7dff5..62f195de7 100644
--- a/__e2e__/flows/composer.yml
+++ b/__e2e__/flows/composer.yml
@@ -3,13 +3,17 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: ?users
+      SERVER_PATH: ?users
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
+
 - assertVisible:
-      id: "composeFAB"
+    id: "composeFAB"
 - tapOn:
     id: "composeFAB"
 - inputText: "Post text only"
@@ -21,7 +25,10 @@ appId: xyz.blueskyweb.app
     id: "composeFAB"
 - inputText: "Post with an image"
 - tapOn:
-    id: "openGalleryBtn"
+    id: "openMediaBtn"
+- extendedWaitUntil:
+    visible:
+      id: "selectedPhotosView"
 - tapOn:
     id: "composerPublishBtn"
 - assertVisible:
@@ -46,7 +53,10 @@ appId: xyz.blueskyweb.app
     id: "replyBtn"
 - inputText: "Reply with an image"
 - tapOn:
-    id: "openGalleryBtn"
+    id: "openMediaBtn"
+- extendedWaitUntil:
+    visible:
+      id: "selectedPhotosView"
 - tapOn:
     id: "composerPublishBtn"
 - assertVisible:
@@ -73,7 +83,10 @@ appId: xyz.blueskyweb.app
     id: "quoteBtn"
 - inputText: "QP with an image"
 - tapOn:
-    id: "openGalleryBtn"
+    id: "openMediaBtn"
+- extendedWaitUntil:
+    visible:
+      id: "selectedPhotosView"
 - tapOn:
     id: "composerPublishBtn"
 - assertVisible:
diff --git a/__e2e__/flows/create-account.yml b/__e2e__/flows/create-account.yml
index 4d49e9b6a..1290c94bf 100644
--- a/__e2e__/flows/create-account.yml
+++ b/__e2e__/flows/create-account.yml
@@ -32,6 +32,9 @@ appId: xyz.blueskyweb.app
     text: "Not Now"
     optional: true
 - inputText: "e2e-test"
+- extendedWaitUntil:
+    visible:
+        id: "handleAvailableCheck"
 - tapOn:
     id: "nextBtn"
-
+- assertVisible: "Give your profile a face"
diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml
index fdb71ae7e..8fe6a2c44 100644
--- a/__e2e__/flows/curate-lists.yml
+++ b/__e2e__/flows/curate-lists.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users&follows&posts"
+      SERVER_PATH: "?users&follows&posts"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 
 - tapOn:
     label: "Create a curate list"
@@ -75,9 +78,8 @@ appId: xyz.blueskyweb.app
 - tapOn:
     id: "confirmBtn"
 
-- tapOn:
-    label: "Create a new curatelist"
-    id: "e2eGotoLists"
+- assertVisible:
+    id: "newUserListBtn"
 - tapOn:
     id: "newUserListBtn"
 - assertVisible:
@@ -146,7 +148,7 @@ appId: xyz.blueskyweb.app
 
 - tapOn:
     id: "bottomBarSearchBtn"
-- tapOn: "Search for posts, users, or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
 - inputText: "bob"
 - tapOn:
     id: "searchAutoCompleteResult-bob.test"
diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml
index ef757c5b1..02d76b83e 100644
--- a/__e2e__/flows/mod-lists.yml
+++ b/__e2e__/flows/mod-lists.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users&follows&labels"
+      SERVER_PATH: "?users&follows&labels"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 
 # create a modlist
 - tapOn:
@@ -28,20 +31,17 @@ appId: xyz.blueskyweb.app
 - assertVisible: "Muted Users"
 - assertVisible: "Shhh"
 
-- tapOn:
-    label: "Dropdown"
-    point: "71%,9%"
-
+# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
+# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
+# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
+- tapOn: "Subscribe to this list"
 - tapOn: "Mute accounts"
 - tapOn: "Mute list"
 - tapOn: "Unmute"
 
-- tapOn:
-      label: "Dropdown"
-      point: "71%,9%"
-
+- tapOn: "Subscribe to this list"
 - tapOn: "Block accounts"
 - tapOn: "Block list"
 - tapOn: "Unblock"
 
- # the rest of the behaviors are tested in curate-lists.yml
+  # the rest of the behaviors are tested in curate-lists.yml
diff --git a/__e2e__/flows/onboarding-avatar-creator.yml b/__e2e__/flows/onboarding-avatar-creator.yml
index 3a20053ba..b8f65b9c3 100644
--- a/__e2e__/flows/onboarding-avatar-creator.yml
+++ b/__e2e__/flows/onboarding-avatar-creator.yml
@@ -3,11 +3,15 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users"
+      SERVER_PATH: "?users"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
+
 - tapOn:
     id: "e2eStartOnboarding"
 - tapOn: "Open avatar creator"
@@ -21,12 +25,12 @@ appId: xyz.blueskyweb.app
 - tapOn: "Select the atom emoji as your avatar"
 - tapOn: "Done"
 - waitForAnimationToEnd
-- tapOn: "Continue to next step"
+- tapOn:
+    id: "onboardingContinue"
 - assertVisible: "What are your interests?"
 - tapOn:
-    label: "Tap on continue"
-    point: "50%,92%"
+    id: "onboardingContinue"
 - assertVisible: "You're ready to go!"
 - tapOn:
-    label: "Tap on Lets go"
-    point: "50%,92%"
\ No newline at end of file
+    id: "onboardingFinish"
+- assertVisible: "Following"
diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml
index c8d9d5723..81aeef6cc 100644
--- a/__e2e__/flows/onboarding.yml
+++ b/__e2e__/flows/onboarding.yml
@@ -3,11 +3,15 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users"
+      SERVER_PATH: "?users"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
+
 - tapOn:
     id: "e2eStartOnboarding"
 - tapOn: "Select an avatar"
@@ -18,12 +22,12 @@ appId: xyz.blueskyweb.app
 - waitForAnimationToEnd
 - tapOn: "Done"
 - waitForAnimationToEnd
-- tapOn: "Continue to next step"
+- tapOn:
+    id: "onboardingContinue"
 - assertVisible: "What are your interests?"
 - tapOn:
-    label: "Tap on continue"
-    point: "50%,92%"
+    id: "onboardingContinue"
 - assertVisible: "You're ready to go!"
 - tapOn:
-    label: "Tap on Lets go"
-    point: "50%,92%"
\ No newline at end of file
+    id: "onboardingFinish"
+- assertVisible: "Following"
diff --git a/__e2e__/flows/post-report-flow.yml b/__e2e__/flows/post-report-flow.yml
index 3925bef89..a509fcc02 100644
--- a/__e2e__/flows/post-report-flow.yml
+++ b/__e2e__/flows/post-report-flow.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users&follows&posts"
+      SERVER_PATH: "?users&follows&posts"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 
 - tapOn:
     id: "postDropdownBtn"
diff --git a/__e2e__/flows/profile-screen.yml b/__e2e__/flows/profile-screen.yml
index 858448b44..a20302dac 100644
--- a/__e2e__/flows/profile-screen.yml
+++ b/__e2e__/flows/profile-screen.yml
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users&posts&feeds"
+      SERVER_PATH: "?users&posts&feeds"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
@@ -12,10 +12,10 @@ appId: xyz.blueskyweb.app
 # Navigate to another user profile
 - extendedWaitUntil:
     visible:
-        id: "bottomBarSearchBtn"
+      id: "bottomBarSearchBtn"
 - tapOn:
     id: "bottomBarSearchBtn"
-- tapOn: "Search for posts, users, or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
 - inputText: "b"
 - tapOn:
     id: "searchAutoCompleteResult-bob.test"
@@ -38,4 +38,4 @@ appId: xyz.blueskyweb.app
 - tapOn:
     id: "profileHeaderDropdownBtn"
 - tapOn: "Unmute Account"
-- assertNotVisible: "Account Muted"
\ No newline at end of file
+- assertNotVisible: "Account Muted"
diff --git a/__e2e__/flows/search-screen.yml b/__e2e__/flows/search-screen.yml
index 78ebbee36..13a1b06fa 100644
--- a/__e2e__/flows/search-screen.yml
+++ b/__e2e__/flows/search-screen.yml
@@ -3,20 +3,22 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users"
+      SERVER_PATH: "?users"
 - runFlow:
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
 
 # Navigate to another user profile via autocomplete
+- extendedWaitUntil:
+    visible:
+      id: "bottomBarSearchBtn"
 - tapOn:
     id: "bottomBarSearchBtn"
-- assertVisible: "Search for posts, users, or feeds"
-- tapOn: "Search for posts, users, or feeds"
+- assertVisible: "Search for posts, users[,]? or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
 - inputText: "b"
 - tapOn:
     id: "searchAutoCompleteResult-bob.test"
 - assertVisible:
     id: "profileView"
-
diff --git a/__e2e__/flows/shared-prefs.yml b/__e2e__/flows/shared-prefs.yml
index 9cc7707be..8b773a766 100644
--- a/__e2e__/flows/shared-prefs.yml
+++ b/__e2e__/flows/shared-prefs.yml
@@ -8,8 +8,12 @@ appId: xyz.blueskyweb.app
     file: ../setupApp.yml
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
+
 - assertVisible:
-      id: "storybookBtn"
+    id: "storybookBtn"
 - tapOn:
     id: "storybookBtn"
 - tapOn:
diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml
index e588805c2..2724833fe 100644
--- a/__e2e__/flows/thread-muting.yml
+++ b/__e2e__/flows/thread-muting.yml
@@ -3,13 +3,16 @@ appId: xyz.blueskyweb.app
 - runScript:
     file: ../setupServer.js
     env:
-        SERVER_PATH: "?users&follows"
+      SERVER_PATH: "?users&follows"
 - runFlow:
     file: ../setupApp.yml
 
 # Login, create a thread, and log out
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 - assertVisible:
     id: "composeFAB"
 - tapOn:
@@ -20,7 +23,12 @@ appId: xyz.blueskyweb.app
 
 # Login, reply to the thread, and log out
 - tapOn:
+    id: "e2eSignOut"
+- tapOn:
     id: "e2eSignInBob"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 - tapOn:
     id: "replyBtn"
 - inputText: "Reply 1"
@@ -29,7 +37,12 @@ appId: xyz.blueskyweb.app
 
 # Login, confirm notification exists, mute thread, and log out
 - tapOn:
+    id: "e2eSignOut"
+- tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 - tapOn:
     id: "bottomBarNotificationsBtn"
 - assertVisible:
@@ -39,12 +52,17 @@ appId: xyz.blueskyweb.app
 - tapOn:
     id: "postDropdownBtn"
     childOf:
-        id: "postThreadItem-by-bob.test"
+      id: "postThreadItem-by-bob.test"
 - tapOn: "Mute thread"
 
 # Login, reply to the thread twice, and log out
 - tapOn:
+    id: "e2eSignOut"
+- tapOn:
     id: "e2eSignInBob"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 - tapOn:
     id: "bottomBarProfileBtn"
 - tapOn:
@@ -60,24 +78,35 @@ appId: xyz.blueskyweb.app
 - tapOn:
     id: "composerPublishBtn"
 
-
-# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist
+# 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
+- tapOn:
+    id: "e2eSignOut"
 - tapOn:
     id: "e2eSignInAlice"
+- extendedWaitUntil:
+    visible:
+      id: "viewHeaderHomeFeedPrefsBtn"
 - tapOn:
     id: "bottomBarNotificationsBtn"
-- assertNotVisible:
+- assertVisible: ".*Reply 1.*"
+- assertNotVisible: ".*Reply 2.*"
+- assertNotVisible: ".*Reply 3.*"
+- assertVisible:
     id: "feedItem-by-bob.test"
 - tapOn:
-    id: "bottomBarHomeBtn"
+    id: "feedItem-by-bob.test"
 - tapOn:
     id: "postDropdownBtn"
+    childOf:
+      id: "postThreadItem-by-bob.test"
 - tapOn: "Unmute thread"
 - tapOn:
     id: "bottomBarNotificationsBtn"
 - swipe:
     from:
-        id: "notifsFeed"
+      id: "notifsFeed"
     direction: DOWN
-- assertVisible:
-    id: "feedItem-by-bob.test"
+- assertVisible: ".*Reply 1.*"
+- assertNotVisible: ".*Reply 2.*"
+- assertNotVisible: ".*Reply 3.*"
diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts
index d4be040df..1f7884844 100644
--- a/__e2e__/mock-server.ts
+++ b/__e2e__/mock-server.ts
@@ -181,10 +181,10 @@ async function main() {
             'warn-profile',
             'warn-posts',
             'muted-account',
-            'muted-by-list-account',
+            'muted-by-list-acc',
             'blocking-account',
             'blockedby-account',
-            'mutual-block-account',
+            'mutual-block-acc',
           ]) {
             await server.mocker.createUser(user)
             await server.mocker.follow('alice', user)
@@ -411,16 +411,16 @@ async function main() {
           await server.mocker.addToMuteList(
             'alice',
             list,
-            server.mocker.users['muted-by-list-account'].did,
+            server.mocker.users['muted-by-list-acc'].did,
           )
-          await server.mocker.createPost('muted-by-list-account', 'muted post')
+          await server.mocker.createPost('muted-by-list-acc', 'muted post')
           await server.mocker.createQuotePost(
-            'muted-by-list-account',
+            'muted-by-list-acc',
             'account quote post',
             anchorPost,
           )
           await server.mocker.createReply(
-            'muted-by-list-account',
+            'muted-by-list-acc',
             'account reply',
             anchorPost,
           )
@@ -470,16 +470,16 @@ async function main() {
           )
 
           await server.mocker.createPost(
-            'mutual-block-account',
+            'mutual-block-acc',
             'mutual-block post',
           )
           await server.mocker.createQuotePost(
-            'mutual-block-account',
+            'mutual-block-acc',
             'mutual-block quote post',
             anchorPost,
           )
           await server.mocker.createReply(
-            'mutual-block-account',
+            'mutual-block-acc',
             'mutual-block reply',
             anchorPost,
           )
@@ -488,15 +488,15 @@ async function main() {
               repo: server.mocker.users.alice.did,
             },
             {
-              subject: server.mocker.users['mutual-block-account'].did,
+              subject: server.mocker.users['mutual-block-acc'].did,
               createdAt: new Date().toISOString(),
             },
           )
           await server.mocker.users[
-            'mutual-block-account'
+            'mutual-block-acc'
           ].agent.app.bsky.graph.block.create(
             {
-              repo: server.mocker.users['mutual-block-account'].did,
+              repo: server.mocker.users['mutual-block-acc'].did,
             },
             {
               subject: server.mocker.users.alice.did,
diff --git a/__e2e__/setupServer.js b/__e2e__/setupServer.js
index dedf4ffa9..5038197b0 100644
--- a/__e2e__/setupServer.js
+++ b/__e2e__/setupServer.js
@@ -1,8 +1,8 @@
-// eslint-disable-next-line
+// eslint-disable-next-line no-undef
 var res = http.post('http://localhost:1986/' + SERVER_PATH, {
   headers: {'Content-Type': 'text/plain'},
   body: '',
 })
 
-// eslint-disable-next-line
+// eslint-disable-next-line no-undef
 output.result = json(res.body).appviewDid
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
index a2a9357ec..92e1495e5 100644
--- a/src/lib/media/picker.e2e.tsx
+++ b/src/lib/media/picker.e2e.tsx
@@ -7,6 +7,7 @@ import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool'
 
 import {compressIfNeeded} from './manip'
 import {type PickerImage} from './picker.shared'
+import {ImagePickerResult} from 'expo-image-picker'
 
 async function getFile() {
   const imagesDir = documentDirectory!
@@ -38,6 +39,22 @@ export async function openPicker(): Promise<PickerImage[]> {
   return [await getFile()]
 }
 
+export async function openUnifiedPicker(): Promise<ImagePickerResult> {
+  const file = await getFile()
+
+  return {
+    assets: [
+      {
+        type: 'image',
+        uri: file.path,
+        mimeType: file.mime,
+        ...file,
+      },
+    ],
+    canceled: false,
+  }
+}
+
 export async function openCamera(): Promise<PickerImage> {
   return await getFile()
 }
diff --git a/src/lib/media/picker.shared.ts b/src/lib/media/picker.shared.ts
index 8ec1154c8..6df712e9a 100644
--- a/src/lib/media/picker.shared.ts
+++ b/src/lib/media/picker.shared.ts
@@ -1,11 +1,14 @@
 import {
   type ImagePickerOptions,
   launchImageLibraryAsync,
+  UIImagePickerPreferredAssetRepresentationMode,
 } from 'expo-image-picker'
 import {t} from '@lingui/macro'
 
+import {isIOS, isWeb} from '#/platform/detection'
 import {type ImageMeta} from '#/state/gallery'
 import * as Toast from '#/view/com/util/Toast'
+import {VIDEO_MAX_DURATION_MS} from '../constants'
 import {getDataUriSize} from './util'
 
 export type PickerImage = ImageMeta & {
@@ -36,3 +39,22 @@ export async function openPicker(opts?: ImagePickerOptions) {
       size: getDataUriSize(image.uri),
     }))
 }
+
+export async function openUnifiedPicker({
+  selectionCountRemaining,
+}: {
+  selectionCountRemaining: number
+}) {
+  return await launchImageLibraryAsync({
+    exif: false,
+    mediaTypes: ['images', 'videos'],
+    quality: 1,
+    allowsMultipleSelection: true,
+    legacy: true,
+    base64: isWeb,
+    selectionLimit: isIOS ? selectionCountRemaining : undefined,
+    preferredAssetRepresentationMode:
+      UIImagePickerPreferredAssetRepresentationMode.Current,
+    videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
+  })
+}
diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx
index 6095730d5..2526da3c8 100644
--- a/src/lib/media/picker.tsx
+++ b/src/lib/media/picker.tsx
@@ -1,7 +1,11 @@
 import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool'
 import {type ImagePickerOptions, launchCameraAsync} from 'expo-image-picker'
 
-export {openPicker, type PickerImage as RNImage} from './picker.shared'
+export {
+  openPicker,
+  openUnifiedPicker,
+  type PickerImage as RNImage,
+} from './picker.shared'
 
 export async function openCamera(customOpts: ImagePickerOptions) {
   const opts: ImagePickerOptions = {
diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx
index c1e4e4ab7..233600583 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -3,7 +3,7 @@ import {type OpenCropperOptions} from 'expo-image-crop-tool'
 import {type PickerImage} from './picker.shared'
 import {type CameraOpts} from './types'
 
-export {openPicker} from './picker.shared'
+export {openPicker, openUnifiedPicker} from './picker.shared'
 
 export async function openCamera(_opts: CameraOpts): Promise<PickerImage> {
   throw new Error('openCamera is not supported on web')
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index f8040f3a5..c4b723ce1 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -580,6 +580,7 @@ function LegacyFinalStep({
 
       <OnboardingControls.Portal>
         <Button
+          testID="onboardingFinish"
           disabled={saving}
           key={state.activeStep} // remove focus state on nav
           color="primary"
diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx
index 3bde22136..d214937d4 100644
--- a/src/screens/Onboarding/StepInterests/index.tsx
+++ b/src/screens/Onboarding/StepInterests/index.tsx
@@ -244,6 +244,7 @@ export function StepInterests() {
         ) : (
           <Button
             disabled={saving || !data}
+            testID="onboardingContinue"
             variant="solid"
             color="primary"
             size="large"
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index fd5f9b6fb..1725daee3 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -268,6 +268,7 @@ export function StepProfile() {
         <OnboardingControls.Portal>
           <View style={[a.gap_md, gtMobile && a.flex_row_reverse]}>
             <Button
+              testID="onboardingContinue"
               variant="solid"
               color="primary"
               size="large"
@@ -279,6 +280,7 @@ export function StepProfile() {
               <ButtonIcon icon={ChevronRight} position="right" />
             </Button>
             <Button
+              testID="onboardingAvatarCreator"
               variant="ghost"
               color="primary"
               size="large"
diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx
index 2291e5e4f..f13402ece 100644
--- a/src/screens/Onboarding/index.tsx
+++ b/src/screens/Onboarding/index.tsx
@@ -13,13 +13,15 @@ import {StepFinished} from '#/screens/Onboarding/StepFinished'
 import {StepInterests} from '#/screens/Onboarding/StepInterests'
 import {StepProfile} from '#/screens/Onboarding/StepProfile'
 import {Portal} from '#/components/Portal'
+import {ENV} from '#/env'
 import {StepSuggestedAccounts} from './StepSuggestedAccounts'
 
 export function Onboarding() {
   const {_} = useLingui()
   const gate = useGate()
-  const showValueProp = gate('onboarding_value_prop')
-  const showSuggestedAccounts = gate('onboarding_suggested_accounts')
+  const showValueProp = ENV !== 'e2e' && gate('onboarding_value_prop')
+  const showSuggestedAccounts =
+    ENV !== 'e2e' && gate('onboarding_suggested_accounts')
   const [state, dispatch] = useReducer(reducer, {
     ...initialState,
     totalSteps: showSuggestedAccounts ? 4 : 3,
diff --git a/src/screens/Signup/StepHandle/index.tsx b/src/screens/Signup/StepHandle/index.tsx
index 5bf6b2269..64333933c 100644
--- a/src/screens/Signup/StepHandle/index.tsx
+++ b/src/screens/Signup/StepHandle/index.tsx
@@ -168,7 +168,10 @@ export function StepHandle() {
               </TextField.GhostText>
             )}
             {isHandleAvailable?.available && (
-              <CheckIcon style={[{color: t.palette.positive_600}, a.z_20]} />
+              <CheckIcon
+                testID="handleAvailableCheck"
+                style={[{color: t.palette.positive_600}, a.z_20]}
+              />
             )}
           </TextField.Root>
         </View>
diff --git a/src/view/com/composer/SelectMediaButton.tsx b/src/view/com/composer/SelectMediaButton.tsx
index b76a017b4..9401b7975 100644
--- a/src/view/com/composer/SelectMediaButton.tsx
+++ b/src/view/com/composer/SelectMediaButton.tsx
@@ -1,10 +1,6 @@
 import {useCallback} from 'react'
 import {Keyboard} from 'react-native'
-import {
-  type ImagePickerAsset,
-  launchImageLibraryAsync,
-  UIImagePickerPreferredAssetRepresentationMode,
-} from 'expo-image-picker'
+import {type ImagePickerAsset} from 'expo-image-picker'
 import {msg, plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -13,8 +9,9 @@ import {
   usePhotoLibraryPermission,
   useVideoLibraryPermission,
 } from '#/lib/hooks/usePermissions'
+import {openUnifiedPicker} from '#/lib/media/picker'
 import {extractDataUriMime} from '#/lib/media/util'
-import {isIOS, isNative, isWeb} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
 import {MAX_IMAGES} from '#/view/com/composer/state/composer'
 import {atoms as a, useTheme} from '#/alf'
 import {Button} from '#/components/Button'
@@ -448,18 +445,7 @@ export function SelectMediaButton({
     }
 
     const {assets, canceled} = await sheetWrapper(
-      launchImageLibraryAsync({
-        exif: false,
-        mediaTypes: ['images', 'videos'],
-        quality: 1,
-        allowsMultipleSelection: true,
-        legacy: true,
-        base64: isWeb,
-        selectionLimit: isIOS ? selectionCountRemaining : undefined,
-        preferredAssetRepresentationMode:
-          UIImagePickerPreferredAssetRepresentationMode.Current,
-        videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
-      }),
+      openUnifiedPicker({selectionCountRemaining}),
     )
 
     if (canceled) return