An ATproto social media client -- with an independent Appview.

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

authored by samuel.fm and committed by

GitHub d6dc52b6 541502c7

+200 -92
+9 -3
__e2e__/flows/composer-self-label.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: ?users 6 + SERVER_PATH: ?users 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 11 14 12 15 # Post an image with the porn label 13 16 - assertVisible: 14 - id: "composeFAB" 17 + id: "composeFAB" 15 18 - tapOn: 16 19 id: "composeFAB" 17 20 - inputText: "Post with an image" 18 21 - tapOn: 19 - id: "openGalleryBtn" 22 + id: "openMediaBtn" 23 + - extendedWaitUntil: 24 + visible: 25 + id: "selectedPhotosView" 20 26 - tapOn: "Content warnings" 21 27 - tapOn: "Porn" 22 28 - tapOn:
+18 -5
__e2e__/flows/composer.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: ?users 6 + SERVER_PATH: ?users 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 14 + 11 15 - assertVisible: 12 - id: "composeFAB" 16 + id: "composeFAB" 13 17 - tapOn: 14 18 id: "composeFAB" 15 19 - inputText: "Post text only" ··· 21 25 id: "composeFAB" 22 26 - inputText: "Post with an image" 23 27 - tapOn: 24 - id: "openGalleryBtn" 28 + id: "openMediaBtn" 29 + - extendedWaitUntil: 30 + visible: 31 + id: "selectedPhotosView" 25 32 - tapOn: 26 33 id: "composerPublishBtn" 27 34 - assertVisible: ··· 46 53 id: "replyBtn" 47 54 - inputText: "Reply with an image" 48 55 - tapOn: 49 - id: "openGalleryBtn" 56 + id: "openMediaBtn" 57 + - extendedWaitUntil: 58 + visible: 59 + id: "selectedPhotosView" 50 60 - tapOn: 51 61 id: "composerPublishBtn" 52 62 - assertVisible: ··· 73 83 id: "quoteBtn" 74 84 - inputText: "QP with an image" 75 85 - tapOn: 76 - id: "openGalleryBtn" 86 + id: "openMediaBtn" 87 + - extendedWaitUntil: 88 + visible: 89 + id: "selectedPhotosView" 77 90 - tapOn: 78 91 id: "composerPublishBtn" 79 92 - assertVisible:
+4 -1
__e2e__/flows/create-account.yml
··· 32 32 text: "Not Now" 33 33 optional: true 34 34 - inputText: "e2e-test" 35 + - extendedWaitUntil: 36 + visible: 37 + id: "handleAvailableCheck" 35 38 - tapOn: 36 39 id: "nextBtn" 37 - 40 + - assertVisible: "Give your profile a face"
+7 -5
__e2e__/flows/curate-lists.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users&follows&posts" 6 + SERVER_PATH: "?users&follows&posts" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 11 14 12 15 - tapOn: 13 16 label: "Create a curate list" ··· 75 78 - tapOn: 76 79 id: "confirmBtn" 77 80 78 - - tapOn: 79 - label: "Create a new curatelist" 80 - id: "e2eGotoLists" 81 + - assertVisible: 82 + id: "newUserListBtn" 81 83 - tapOn: 82 84 id: "newUserListBtn" 83 85 - assertVisible: ··· 146 148 147 149 - tapOn: 148 150 id: "bottomBarSearchBtn" 149 - - tapOn: "Search for posts, users, or feeds" 151 + - tapOn: "Search for posts, users[,]? or feeds" 150 152 - inputText: "bob" 151 153 - tapOn: 152 154 id: "searchAutoCompleteResult-bob.test"
+10 -10
__e2e__/flows/mod-lists.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users&follows&labels" 6 + SERVER_PATH: "?users&follows&labels" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 11 14 12 15 # create a modlist 13 16 - tapOn: ··· 28 31 - assertVisible: "Muted Users" 29 32 - assertVisible: "Shhh" 30 33 31 - - tapOn: 32 - label: "Dropdown" 33 - point: "71%,9%" 34 - 34 + # DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE 35 + # IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE 36 + # BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn 37 + - tapOn: "Subscribe to this list" 35 38 - tapOn: "Mute accounts" 36 39 - tapOn: "Mute list" 37 40 - tapOn: "Unmute" 38 41 39 - - tapOn: 40 - label: "Dropdown" 41 - point: "71%,9%" 42 - 42 + - tapOn: "Subscribe to this list" 43 43 - tapOn: "Block accounts" 44 44 - tapOn: "Block list" 45 45 - tapOn: "Unblock" 46 46 47 - # the rest of the behaviors are tested in curate-lists.yml 47 + # the rest of the behaviors are tested in curate-lists.yml
+10 -6
__e2e__/flows/onboarding-avatar-creator.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users" 6 + SERVER_PATH: "?users" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 14 + 11 15 - tapOn: 12 16 id: "e2eStartOnboarding" 13 17 - tapOn: "Open avatar creator" ··· 21 25 - tapOn: "Select the atom emoji as your avatar" 22 26 - tapOn: "Done" 23 27 - waitForAnimationToEnd 24 - - tapOn: "Continue to next step" 28 + - tapOn: 29 + id: "onboardingContinue" 25 30 - assertVisible: "What are your interests?" 26 31 - tapOn: 27 - label: "Tap on continue" 28 - point: "50%,92%" 32 + id: "onboardingContinue" 29 33 - assertVisible: "You're ready to go!" 30 34 - tapOn: 31 - label: "Tap on Lets go" 32 - point: "50%,92%" 35 + id: "onboardingFinish" 36 + - assertVisible: "Following"
+10 -6
__e2e__/flows/onboarding.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users" 6 + SERVER_PATH: "?users" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 14 + 11 15 - tapOn: 12 16 id: "e2eStartOnboarding" 13 17 - tapOn: "Select an avatar" ··· 18 22 - waitForAnimationToEnd 19 23 - tapOn: "Done" 20 24 - waitForAnimationToEnd 21 - - tapOn: "Continue to next step" 25 + - tapOn: 26 + id: "onboardingContinue" 22 27 - assertVisible: "What are your interests?" 23 28 - tapOn: 24 - label: "Tap on continue" 25 - point: "50%,92%" 29 + id: "onboardingContinue" 26 30 - assertVisible: "You're ready to go!" 27 31 - tapOn: 28 - label: "Tap on Lets go" 29 - point: "50%,92%" 32 + id: "onboardingFinish" 33 + - assertVisible: "Following"
+4 -1
__e2e__/flows/post-report-flow.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users&follows&posts" 6 + SERVER_PATH: "?users&follows&posts" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 11 14 12 15 - tapOn: 13 16 id: "postDropdownBtn"
+4 -4
__e2e__/flows/profile-screen.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users&posts&feeds" 6 + SERVER_PATH: "?users&posts&feeds" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: ··· 12 12 # Navigate to another user profile 13 13 - extendedWaitUntil: 14 14 visible: 15 - id: "bottomBarSearchBtn" 15 + id: "bottomBarSearchBtn" 16 16 - tapOn: 17 17 id: "bottomBarSearchBtn" 18 - - tapOn: "Search for posts, users, or feeds" 18 + - tapOn: "Search for posts, users[,]? or feeds" 19 19 - inputText: "b" 20 20 - tapOn: 21 21 id: "searchAutoCompleteResult-bob.test" ··· 38 38 - tapOn: 39 39 id: "profileHeaderDropdownBtn" 40 40 - tapOn: "Unmute Account" 41 - - assertNotVisible: "Account Muted" 41 + - assertNotVisible: "Account Muted"
+6 -4
__e2e__/flows/search-screen.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users" 6 + SERVER_PATH: "?users" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 11 12 12 # Navigate to another user profile via autocomplete 13 + - extendedWaitUntil: 14 + visible: 15 + id: "bottomBarSearchBtn" 13 16 - tapOn: 14 17 id: "bottomBarSearchBtn" 15 - - assertVisible: "Search for posts, users, or feeds" 16 - - tapOn: "Search for posts, users, or feeds" 18 + - assertVisible: "Search for posts, users[,]? or feeds" 19 + - tapOn: "Search for posts, users[,]? or feeds" 17 20 - inputText: "b" 18 21 - tapOn: 19 22 id: "searchAutoCompleteResult-bob.test" 20 23 - assertVisible: 21 24 id: "profileView" 22 -
+5 -1
__e2e__/flows/shared-prefs.yml
··· 8 8 file: ../setupApp.yml 9 9 - tapOn: 10 10 id: "e2eSignInAlice" 11 + - extendedWaitUntil: 12 + visible: 13 + id: "viewHeaderHomeFeedPrefsBtn" 14 + 11 15 - assertVisible: 12 - id: "storybookBtn" 16 + id: "storybookBtn" 13 17 - tapOn: 14 18 id: "storybookBtn" 15 19 - tapOn:
+38 -9
__e2e__/flows/thread-muting.yml
··· 3 3 - runScript: 4 4 file: ../setupServer.js 5 5 env: 6 - SERVER_PATH: "?users&follows" 6 + SERVER_PATH: "?users&follows" 7 7 - runFlow: 8 8 file: ../setupApp.yml 9 9 10 10 # Login, create a thread, and log out 11 11 - tapOn: 12 12 id: "e2eSignInAlice" 13 + - extendedWaitUntil: 14 + visible: 15 + id: "viewHeaderHomeFeedPrefsBtn" 13 16 - assertVisible: 14 17 id: "composeFAB" 15 18 - tapOn: ··· 20 23 21 24 # Login, reply to the thread, and log out 22 25 - tapOn: 26 + id: "e2eSignOut" 27 + - tapOn: 23 28 id: "e2eSignInBob" 29 + - extendedWaitUntil: 30 + visible: 31 + id: "viewHeaderHomeFeedPrefsBtn" 24 32 - tapOn: 25 33 id: "replyBtn" 26 34 - inputText: "Reply 1" ··· 28 36 id: "composerPublishBtn" 29 37 30 38 # Login, confirm notification exists, mute thread, and log out 39 + - tapOn: 40 + id: "e2eSignOut" 31 41 - tapOn: 32 42 id: "e2eSignInAlice" 43 + - extendedWaitUntil: 44 + visible: 45 + id: "viewHeaderHomeFeedPrefsBtn" 33 46 - tapOn: 34 47 id: "bottomBarNotificationsBtn" 35 48 - assertVisible: ··· 39 52 - tapOn: 40 53 id: "postDropdownBtn" 41 54 childOf: 42 - id: "postThreadItem-by-bob.test" 55 + id: "postThreadItem-by-bob.test" 43 56 - tapOn: "Mute thread" 44 57 45 58 # Login, reply to the thread twice, and log out 46 59 - tapOn: 60 + id: "e2eSignOut" 61 + - tapOn: 47 62 id: "e2eSignInBob" 63 + - extendedWaitUntil: 64 + visible: 65 + id: "viewHeaderHomeFeedPrefsBtn" 48 66 - tapOn: 49 67 id: "bottomBarProfileBtn" 50 68 - tapOn: ··· 60 78 - tapOn: 61 79 id: "composerPublishBtn" 62 80 63 - 64 - # Login, confirm notifications dont exist, unmute the thread, confirm notifications exist 81 + # Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~ 82 + # Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn 83 + - tapOn: 84 + id: "e2eSignOut" 65 85 - tapOn: 66 86 id: "e2eSignInAlice" 87 + - extendedWaitUntil: 88 + visible: 89 + id: "viewHeaderHomeFeedPrefsBtn" 67 90 - tapOn: 68 91 id: "bottomBarNotificationsBtn" 69 - - assertNotVisible: 92 + - assertVisible: ".*Reply 1.*" 93 + - assertNotVisible: ".*Reply 2.*" 94 + - assertNotVisible: ".*Reply 3.*" 95 + - assertVisible: 70 96 id: "feedItem-by-bob.test" 71 97 - tapOn: 72 - id: "bottomBarHomeBtn" 98 + id: "feedItem-by-bob.test" 73 99 - tapOn: 74 100 id: "postDropdownBtn" 101 + childOf: 102 + id: "postThreadItem-by-bob.test" 75 103 - tapOn: "Unmute thread" 76 104 - tapOn: 77 105 id: "bottomBarNotificationsBtn" 78 106 - swipe: 79 107 from: 80 - id: "notifsFeed" 108 + id: "notifsFeed" 81 109 direction: DOWN 82 - - assertVisible: 83 - id: "feedItem-by-bob.test" 110 + - assertVisible: ".*Reply 1.*" 111 + - assertNotVisible: ".*Reply 2.*" 112 + - assertNotVisible: ".*Reply 3.*"
+12 -12
__e2e__/mock-server.ts
··· 181 181 'warn-profile', 182 182 'warn-posts', 183 183 'muted-account', 184 - 'muted-by-list-account', 184 + 'muted-by-list-acc', 185 185 'blocking-account', 186 186 'blockedby-account', 187 - 'mutual-block-account', 187 + 'mutual-block-acc', 188 188 ]) { 189 189 await server.mocker.createUser(user) 190 190 await server.mocker.follow('alice', user) ··· 411 411 await server.mocker.addToMuteList( 412 412 'alice', 413 413 list, 414 - server.mocker.users['muted-by-list-account'].did, 414 + server.mocker.users['muted-by-list-acc'].did, 415 415 ) 416 - await server.mocker.createPost('muted-by-list-account', 'muted post') 416 + await server.mocker.createPost('muted-by-list-acc', 'muted post') 417 417 await server.mocker.createQuotePost( 418 - 'muted-by-list-account', 418 + 'muted-by-list-acc', 419 419 'account quote post', 420 420 anchorPost, 421 421 ) 422 422 await server.mocker.createReply( 423 - 'muted-by-list-account', 423 + 'muted-by-list-acc', 424 424 'account reply', 425 425 anchorPost, 426 426 ) ··· 470 470 ) 471 471 472 472 await server.mocker.createPost( 473 - 'mutual-block-account', 473 + 'mutual-block-acc', 474 474 'mutual-block post', 475 475 ) 476 476 await server.mocker.createQuotePost( 477 - 'mutual-block-account', 477 + 'mutual-block-acc', 478 478 'mutual-block quote post', 479 479 anchorPost, 480 480 ) 481 481 await server.mocker.createReply( 482 - 'mutual-block-account', 482 + 'mutual-block-acc', 483 483 'mutual-block reply', 484 484 anchorPost, 485 485 ) ··· 488 488 repo: server.mocker.users.alice.did, 489 489 }, 490 490 { 491 - subject: server.mocker.users['mutual-block-account'].did, 491 + subject: server.mocker.users['mutual-block-acc'].did, 492 492 createdAt: new Date().toISOString(), 493 493 }, 494 494 ) 495 495 await server.mocker.users[ 496 - 'mutual-block-account' 496 + 'mutual-block-acc' 497 497 ].agent.app.bsky.graph.block.create( 498 498 { 499 - repo: server.mocker.users['mutual-block-account'].did, 499 + repo: server.mocker.users['mutual-block-acc'].did, 500 500 }, 501 501 { 502 502 subject: server.mocker.users.alice.did,
+2 -2
__e2e__/setupServer.js
··· 1 - // eslint-disable-next-line 1 + // eslint-disable-next-line no-undef 2 2 var res = http.post('http://localhost:1986/' + SERVER_PATH, { 3 3 headers: {'Content-Type': 'text/plain'}, 4 4 body: '', 5 5 }) 6 6 7 - // eslint-disable-next-line 7 + // eslint-disable-next-line no-undef 8 8 output.result = json(res.body).appviewDid
+17
src/lib/media/picker.e2e.tsx
··· 7 7 8 8 import {compressIfNeeded} from './manip' 9 9 import {type PickerImage} from './picker.shared' 10 + import {ImagePickerResult} from 'expo-image-picker' 10 11 11 12 async function getFile() { 12 13 const imagesDir = documentDirectory! ··· 36 37 37 38 export async function openPicker(): Promise<PickerImage[]> { 38 39 return [await getFile()] 40 + } 41 + 42 + export async function openUnifiedPicker(): Promise<ImagePickerResult> { 43 + const file = await getFile() 44 + 45 + return { 46 + assets: [ 47 + { 48 + type: 'image', 49 + uri: file.path, 50 + mimeType: file.mime, 51 + ...file, 52 + }, 53 + ], 54 + canceled: false, 55 + } 39 56 } 40 57 41 58 export async function openCamera(): Promise<PickerImage> {
+22
src/lib/media/picker.shared.ts
··· 1 1 import { 2 2 type ImagePickerOptions, 3 3 launchImageLibraryAsync, 4 + UIImagePickerPreferredAssetRepresentationMode, 4 5 } from 'expo-image-picker' 5 6 import {t} from '@lingui/macro' 6 7 8 + import {isIOS, isWeb} from '#/platform/detection' 7 9 import {type ImageMeta} from '#/state/gallery' 8 10 import * as Toast from '#/view/com/util/Toast' 11 + import {VIDEO_MAX_DURATION_MS} from '../constants' 9 12 import {getDataUriSize} from './util' 10 13 11 14 export type PickerImage = ImageMeta & { ··· 36 39 size: getDataUriSize(image.uri), 37 40 })) 38 41 } 42 + 43 + export async function openUnifiedPicker({ 44 + selectionCountRemaining, 45 + }: { 46 + selectionCountRemaining: number 47 + }) { 48 + return await launchImageLibraryAsync({ 49 + exif: false, 50 + mediaTypes: ['images', 'videos'], 51 + quality: 1, 52 + allowsMultipleSelection: true, 53 + legacy: true, 54 + base64: isWeb, 55 + selectionLimit: isIOS ? selectionCountRemaining : undefined, 56 + preferredAssetRepresentationMode: 57 + UIImagePickerPreferredAssetRepresentationMode.Current, 58 + videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000, 59 + }) 60 + }
+5 -1
src/lib/media/picker.tsx
··· 1 1 import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool' 2 2 import {type ImagePickerOptions, launchCameraAsync} from 'expo-image-picker' 3 3 4 - export {openPicker, type PickerImage as RNImage} from './picker.shared' 4 + export { 5 + openPicker, 6 + openUnifiedPicker, 7 + type PickerImage as RNImage, 8 + } from './picker.shared' 5 9 6 10 export async function openCamera(customOpts: ImagePickerOptions) { 7 11 const opts: ImagePickerOptions = {
+1 -1
src/lib/media/picker.web.tsx
··· 3 3 import {type PickerImage} from './picker.shared' 4 4 import {type CameraOpts} from './types' 5 5 6 - export {openPicker} from './picker.shared' 6 + export {openPicker, openUnifiedPicker} from './picker.shared' 7 7 8 8 export async function openCamera(_opts: CameraOpts): Promise<PickerImage> { 9 9 throw new Error('openCamera is not supported on web')
+1
src/screens/Onboarding/StepFinished.tsx
··· 580 580 581 581 <OnboardingControls.Portal> 582 582 <Button 583 + testID="onboardingFinish" 583 584 disabled={saving} 584 585 key={state.activeStep} // remove focus state on nav 585 586 color="primary"
+1
src/screens/Onboarding/StepInterests/index.tsx
··· 244 244 ) : ( 245 245 <Button 246 246 disabled={saving || !data} 247 + testID="onboardingContinue" 247 248 variant="solid" 248 249 color="primary" 249 250 size="large"
+2
src/screens/Onboarding/StepProfile/index.tsx
··· 268 268 <OnboardingControls.Portal> 269 269 <View style={[a.gap_md, gtMobile && a.flex_row_reverse]}> 270 270 <Button 271 + testID="onboardingContinue" 271 272 variant="solid" 272 273 color="primary" 273 274 size="large" ··· 279 280 <ButtonIcon icon={ChevronRight} position="right" /> 280 281 </Button> 281 282 <Button 283 + testID="onboardingAvatarCreator" 282 284 variant="ghost" 283 285 color="primary" 284 286 size="large"
+4 -2
src/screens/Onboarding/index.tsx
··· 13 13 import {StepInterests} from '#/screens/Onboarding/StepInterests' 14 14 import {StepProfile} from '#/screens/Onboarding/StepProfile' 15 15 import {Portal} from '#/components/Portal' 16 + import {ENV} from '#/env' 16 17 import {StepSuggestedAccounts} from './StepSuggestedAccounts' 17 18 18 19 export function Onboarding() { 19 20 const {_} = useLingui() 20 21 const gate = useGate() 21 - const showValueProp = gate('onboarding_value_prop') 22 - const showSuggestedAccounts = gate('onboarding_suggested_accounts') 22 + const showValueProp = ENV !== 'e2e' && gate('onboarding_value_prop') 23 + const showSuggestedAccounts = 24 + ENV !== 'e2e' && gate('onboarding_suggested_accounts') 23 25 const [state, dispatch] = useReducer(reducer, { 24 26 ...initialState, 25 27 totalSteps: showSuggestedAccounts ? 4 : 3,
+4 -1
src/screens/Signup/StepHandle/index.tsx
··· 168 168 </TextField.GhostText> 169 169 )} 170 170 {isHandleAvailable?.available && ( 171 - <CheckIcon style={[{color: t.palette.positive_600}, a.z_20]} /> 171 + <CheckIcon 172 + testID="handleAvailableCheck" 173 + style={[{color: t.palette.positive_600}, a.z_20]} 174 + /> 172 175 )} 173 176 </TextField.Root> 174 177 </View>
+4 -18
src/view/com/composer/SelectMediaButton.tsx
··· 1 1 import {useCallback} from 'react' 2 2 import {Keyboard} from 'react-native' 3 - import { 4 - type ImagePickerAsset, 5 - launchImageLibraryAsync, 6 - UIImagePickerPreferredAssetRepresentationMode, 7 - } from 'expo-image-picker' 3 + import {type ImagePickerAsset} from 'expo-image-picker' 8 4 import {msg, plural} from '@lingui/macro' 9 5 import {useLingui} from '@lingui/react' 10 6 ··· 13 9 usePhotoLibraryPermission, 14 10 useVideoLibraryPermission, 15 11 } from '#/lib/hooks/usePermissions' 12 + import {openUnifiedPicker} from '#/lib/media/picker' 16 13 import {extractDataUriMime} from '#/lib/media/util' 17 - import {isIOS, isNative, isWeb} from '#/platform/detection' 14 + import {isNative, isWeb} from '#/platform/detection' 18 15 import {MAX_IMAGES} from '#/view/com/composer/state/composer' 19 16 import {atoms as a, useTheme} from '#/alf' 20 17 import {Button} from '#/components/Button' ··· 448 445 } 449 446 450 447 const {assets, canceled} = await sheetWrapper( 451 - launchImageLibraryAsync({ 452 - exif: false, 453 - mediaTypes: ['images', 'videos'], 454 - quality: 1, 455 - allowsMultipleSelection: true, 456 - legacy: true, 457 - base64: isWeb, 458 - selectionLimit: isIOS ? selectionCountRemaining : undefined, 459 - preferredAssetRepresentationMode: 460 - UIImagePickerPreferredAssetRepresentationMode.Current, 461 - videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000, 462 - }), 448 + openUnifiedPicker({selectionCountRemaining}), 463 449 ) 464 450 465 451 if (canceled) return