diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 66f9d15de..5314b0859 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -43,30 +43,15 @@ on: - '2' default: '0' - PRINT_FAILED_TEST_LOGS: - description: 'print failed test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' + LOG_LEVEL: + description: 'Test logging verbosity (WARNING: anything other than minimal mode may crash GitHub Actions on large test runs)' required: true type: choice options: - - '0' - - '1' - default: '0' - PRINT_ONGOING_TEST_LOGS: - description: 'print ongoing test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' - required: true - type: choice - options: - - '0' - - '1' - default: '0' - HIDE_WEBDRIVER_LOGS: - description: 'print webdriver logs (1 to hide, 0 to show). PRINT_ONGOING_TEST_LOGS or PRINT_FAILED_TEST_LOGS must be 1' - required: true - type: choice - options: - - '0' - - '1' - default: '1' + - 'minimal' # Recommended for full regressions + - 'failures' # Show failed test logs only + - 'verbose' # All test logs - use with caution! + default: 'minimal' jobs: android-regression: @@ -83,8 +68,9 @@ jobs: APPIUM_ADB_FULL_PATH: '/opt/android/platform-tools/adb' ANDROID_SDK_ROOT: '/opt/android' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.PRINT_FAILED_TEST_LOGS }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.PRINT_ONGOING_TEST_LOGS }} + PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} + PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} + HIDE_WEBDRIVER_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'minimal' && '1' || '0' }} IOS_1_SIMULATOR: '' IOS_2_SIMULATOR: '' IOS_3_SIMULATOR: '' @@ -107,15 +93,15 @@ jobs: uses: ./github/actions/fetch-allure-history if: ${{ env.ALLURE_ENABLED == 'true' }} with: - PLATFORM: ${{env.PLATFORM}} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN}} + PLATFORM: ${{ env.PLATFORM }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: ./github/actions/print-runner-details with: APK_URL: ${{ github.event.inputs.APK_URL }} RISK: ${{ github.event.inputs.RISK }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.PRINT_FAILED_TEST_LOGS }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.PRINT_ONGOING_TEST_LOGS }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - name: Download APK & extract it diff --git a/run/localizer/constants.ts b/run/localizer/constants.ts index 6fd2b45b0..dd2717db3 100644 --- a/run/localizer/constants.ts +++ b/run/localizer/constants.ts @@ -9,14 +9,19 @@ export enum LOCALE_DEFAULTS { token_name_short = 'SESH', usd_name_short = 'USD', app_pro = 'Session Pro', + session_foundation = 'Session Foundation', + pro = 'Pro', } export const rtlLocales = ['ar', 'fa', 'he', 'ps', 'ur']; -export const crowdinLocales = ['en'] as const; +export const crowdinLocales = [ + 'en', +] as const; export type CrowdinLocale = (typeof crowdinLocales)[number]; export function isCrowdinLocale(locale: string): locale is CrowdinLocale { return crowdinLocales.includes(locale as CrowdinLocale); } + diff --git a/run/localizer/locales.ts b/run/localizer/locales.ts index 94b497a5b..17397f859 100644 --- a/run/localizer/locales.ts +++ b/run/localizer/locales.ts @@ -41,7 +41,7 @@ type WithStoreVariant = {storevariant: string}; type WithMin = {min: string}; type WithMax = {max: string}; -export type TokenSimpleNoArgs = +export type TokenSimpleNoArgs = 'about' | 'accept' | 'accountIDCopy' | @@ -81,12 +81,15 @@ export type TokenSimpleNoArgs = 'appIconEnableIconAndName' | 'appIconSelect' | 'appIconSelectionTitle' | + 'appName' | 'appNameCalculator' | 'appNameMeetingSE' | 'appNameNews' | 'appNameNotes' | 'appNameStocks' | 'appNameWeather' | + 'appPro' | + 'appProBadge' | 'appearanceAutoDarkMode' | 'appearanceHideMenuBar' | 'appearanceLanguage' | @@ -198,6 +201,7 @@ export type TokenSimpleNoArgs = 'cameraGrantAccessDescription' | 'cameraGrantAccessQr' | 'cancel' | + 'cancelPlan' | 'change' | 'changePasswordFail' | 'changePasswordModalDescription' | @@ -286,6 +290,8 @@ export type TokenSimpleNoArgs = 'copy' | 'create' | 'creatingCall' | + 'currentPassword' | + 'currentPlan' | 'cut' | 'darkMode' | 'databaseErrorClearDataWarning' | @@ -449,6 +455,7 @@ export type TokenSimpleNoArgs = 'hideOthers' | 'image' | 'images' | + 'important' | 'incognitoKeyboard' | 'incognitoKeyboardDescription' | 'info' | @@ -472,6 +479,7 @@ export type TokenSimpleNoArgs = 'linkPreviewsSendModalDescription' | 'linkPreviewsTurnedOff' | 'linkPreviewsTurnedOffDescription' | + 'links' | 'loadAccount' | 'loadAccountProgressMessage' | 'loading' | @@ -484,7 +492,9 @@ export type TokenSimpleNoArgs = 'lockAppStatus' | 'lockAppUnlock' | 'lockAppUnlocked' | + 'logs' | 'manageMembers' | + 'managePro' | 'max' | 'media' | 'membersAddAccountIdOrOns' | @@ -534,7 +544,10 @@ export type TokenSimpleNoArgs = 'modalMessageCharacterDisplayTitle' | 'modalMessageCharacterTooLongTitle' | 'modalMessageTooLongTitle' | + 'networkName' | + 'newPassword' | 'next' | + 'nextSteps' | 'nicknameEnter' | 'nicknameErrorShorter' | 'nicknameRemove' | @@ -607,6 +620,7 @@ export type TokenSimpleNoArgs = 'open' | 'openSurvey' | 'other' | + 'oxenFoundation' | 'password' | 'passwordChange' | 'passwordChangeShortDescription' | @@ -630,8 +644,8 @@ export type TokenSimpleNoArgs = 'passwordSetShortDescription' | 'passwordStrengthCharLength' | 'passwordStrengthIncludeNumber' | - 'passwordStrengthIncludesLetter' | 'passwordStrengthIncludesLowercase' | + 'passwordStrengthIncludesSymbol' | 'passwordStrengthIncludesUppercase' | 'passwordStrengthIndicator' | 'passwordStrengthIndicatorDescription' | @@ -675,31 +689,68 @@ export type TokenSimpleNoArgs = 'pinConversation' | 'pinUnpin' | 'pinUnpinConversation' | + 'plusLoadsMore' | 'preferences' | 'preview' | 'previewNotification' | + 'pro' | 'proActivated' | + 'proAllSet' | 'proAlreadyPurchased' | 'proAnimatedDisplayPicture' | 'proAnimatedDisplayPictureCallToActionDescription' | 'proAnimatedDisplayPictureFeature' | 'proAnimatedDisplayPictureModalDescription' | + 'proAnimatedDisplayPictures' | + 'proAnimatedDisplayPicturesDescription' | 'proAnimatedDisplayPicturesNonProModalDescription' | 'proBadge' | + 'proBadgeVisible' | + 'proBadges' | + 'proBadgesDescription' | 'proCallToActionLongerMessages' | 'proCallToActionPinnedConversations' | 'proCallToActionPinnedConversationsMoreThan' | + 'proExpired' | + 'proExpiredDescription' | + 'proExpiringSoon' | + 'proFaq' | + 'proFaqDescription' | 'proFeatureListAnimatedDisplayPicture' | 'proFeatureListLargerGroups' | 'proFeatureListLoadsMore' | 'proFeatureListLongerMessages' | 'proFeatureListPinnedConversations' | + 'proFeatures' | 'proGroupActivated' | 'proGroupActivatedDescription' | + 'proImportantDescription' | 'proIncreasedAttachmentSizeFeature' | 'proIncreasedMessageLengthFeature' | + 'proLargerGroups' | + 'proLargerGroupsDescription' | + 'proLongerMessages' | + 'proLongerMessagesDescription' | 'proMessageInfoFeatures' | + 'proPlanNotFound' | + 'proPlanNotFoundDescription' | + 'proPlanRecover' | + 'proPlanRenew' | + 'proPlanRenewStart' | + 'proPlanRenewSupport' | + 'proPlanRestored' | + 'proPlanRestoredDescription' | + 'proRefundDescription' | + 'proRefundRequestSessionSupport' | + 'proRefunding' | + 'proRequestedRefund' | 'proSendMore' | + 'proSettings' | + 'proStats' | + 'proStatsTooltip' | + 'proSupportDescription' | + 'proUnlimitedPins' | + 'proUnlimitedPinsDescription' | 'proUserProfileModalCallToAction' | 'profile' | 'profileDisplayPicture' | @@ -750,7 +801,9 @@ export type TokenSimpleNoArgs = 'remove' | 'removePasswordFail' | 'removePasswordModalDescription' | + 'renew' | 'reply' | + 'requestRefund' | 'resend' | 'resolving' | 'restart' | @@ -784,6 +837,8 @@ export type TokenSimpleNoArgs = 'sessionAppearance' | 'sessionClearData' | 'sessionConversations' | + 'sessionDownloadUrl' | + 'sessionFoundation' | 'sessionHelp' | 'sessionInviteAFriend' | 'sessionMessageRequests' | @@ -799,6 +854,7 @@ export type TokenSimpleNoArgs = 'sessionNotifications' | 'sessionPermissions' | 'sessionPrivacy' | + 'sessionProBeta' | 'sessionRecoveryPassword' | 'sessionSettings' | 'set' | @@ -817,6 +873,7 @@ export type TokenSimpleNoArgs = 'showNoteToSelf' | 'showNoteToSelfDescription' | 'spellChecker' | + 'stakingRewardPool' | 'stickers' | 'strength' | 'supportDescription' | @@ -825,7 +882,10 @@ export type TokenSimpleNoArgs = 'theContinue' | 'theDefault' | 'theError' | + 'theReturn' | 'themePreview' | + 'tokenNameLong' | + 'tokenNameShort' | 'tooltipBlindedIdCommunities' | 'translate' | 'tray' | @@ -847,6 +907,8 @@ export type TokenSimpleNoArgs = 'updateGroupInformationDescription' | 'updateGroupInformationEnterShorterDescription' | 'updateNewVersion' | + 'updatePlan' | + 'updatePlanTwo' | 'updateProfileInformation' | 'updateProfileInformationDescription' | 'updateReleaseNotes' | @@ -858,6 +920,8 @@ export type TokenSimpleNoArgs = 'urlCopy' | 'urlOpen' | 'urlOpenBrowser' | + 'urlOpenDescriptionAlternative' | + 'usdNameShort' | 'useFastMode' | 'video' | 'videoErrorPlay' | @@ -1018,13 +1082,52 @@ export type TokensSimpleAndArgs = { notificationsMutedFor: WithTimeLarge, notificationsMutedForTime: WithDateTime, notificationsSystem: WithMessageCount & WithConversationCount, + onDevice: { device_type: string }, + onDeviceDescription: { device_type: string, platform_account: string }, onboardingBubbleCreatingAnAccountIsEasy: WithEmoji, onboardingBubbleWelcomeToSession: WithEmoji, + openStoreWebsite: { platform_store: string }, passwordErrorLength: WithMin & WithMax, + plusLoadsMoreDescription: WithIcon, + proAllSetDescription: WithDate, + proAutoRenewTime: WithTime, + proBadgesSent: WithCount, + proBilledAnnually: { price: string }, + proBilledMonthly: { price: string }, + proBilledQuarterly: { price: string }, + proDiscountTooltip: { percent: string }, + proExpiringSoonDescription: WithTime, + proGroupsUpgraded: WithCount, + proLongerMessagesSent: WithCount, + proPercentOff: { percent: string }, + proPinnedConversations: WithCount, + proPlanActivatedAuto: WithDate & { current_plan: string }, + proPlanActivatedAutoShort: WithDate & { current_plan: string }, + proPlanActivatedNotAuto: WithDate, + proPlanExpireDate: WithDate, + proPlanPlatformRefund: { platform_store: string, platform_account: string }, + proPlanPlatformRefundLong: { platform_store: string }, + proPlanRenewDesktop: { platform_store: string }, + proPlanRenewDesktopLinked: { platform_store: string }, + proPlanRenewDesktopStore: { platform_store: string, platform_account: string }, + proPlanSignUp: { platform_store: string, platform_account: string }, + proPriceOneMonth: { monthly_price: string }, + proPriceThreeMonths: { monthly_price: string }, + proPriceTwelveMonths: { monthly_price: string }, + proRefundNextSteps: { platform_account: string }, + proRefundRequestStorePolicies: { platform_account: string }, + proRefundSupport: { platform_account: string, platform_store: string }, + proRefundingDescription: { platform_account: string, platform_store: string }, + proTosPrivacy: WithIcon, + proUpdatePlanDescription: WithDate & { current_plan: string, selected_plan: string }, + proUpdatePlanExpireDescription: WithDate & { selected_plan: string }, + processingRefundRequest: { platform_account: string }, rateSessionModalDescription: WithStoreVariant, + refundPlanNonOriginatorApple: { platform_account: string }, remainingCharactersOverTooltip: WithCount, screenshotTaken: WithName, searchMatchesNoneSpecific: WithQuery, + sessionNetworkDataPrice: WithDateTime, sessionNetworkDescription: WithIcon, systemInformationDesktop: WithInformation, tooltipAccountIdVisible: WithName, @@ -1033,7 +1136,8 @@ export type TokensSimpleAndArgs = { updateVersion: WithVersion, updated: WithRelativeTime, urlOpenDescription: WithUrl, - sessionNetworkDataPrice: WithDateTime + viaStoreWebsite: { platform_store: string }, + viaStoreWebsiteDescription: { platform_account: string, platform_store: string } }; export type TokensPluralAndArgs = { @@ -1065,7 +1169,7 @@ export type TokensPluralAndArgs = { searchMatches: WithFoundCount & WithCount }; -export type TokenSimpleWithArgs = +export type TokenSimpleWithArgs = 'accountIdShare' | 'adminMorePromotedToAdmin' | 'adminPromoteDescription' | @@ -1209,13 +1313,52 @@ export type TokenSimpleWithArgs = 'notificationsMutedFor' | 'notificationsMutedForTime' | 'notificationsSystem' | + 'onDevice' | + 'onDeviceDescription' | 'onboardingBubbleCreatingAnAccountIsEasy' | 'onboardingBubbleWelcomeToSession' | + 'openStoreWebsite' | 'passwordErrorLength' | + 'plusLoadsMoreDescription' | + 'proAllSetDescription' | + 'proAutoRenewTime' | + 'proBadgesSent' | + 'proBilledAnnually' | + 'proBilledMonthly' | + 'proBilledQuarterly' | + 'proDiscountTooltip' | + 'proExpiringSoonDescription' | + 'proGroupsUpgraded' | + 'proLongerMessagesSent' | + 'proPercentOff' | + 'proPinnedConversations' | + 'proPlanActivatedAuto' | + 'proPlanActivatedAutoShort' | + 'proPlanActivatedNotAuto' | + 'proPlanExpireDate' | + 'proPlanPlatformRefund' | + 'proPlanPlatformRefundLong' | + 'proPlanRenewDesktop' | + 'proPlanRenewDesktopLinked' | + 'proPlanRenewDesktopStore' | + 'proPlanSignUp' | + 'proPriceOneMonth' | + 'proPriceThreeMonths' | + 'proPriceTwelveMonths' | + 'proRefundNextSteps' | + 'proRefundRequestStorePolicies' | + 'proRefundSupport' | + 'proRefundingDescription' | + 'proTosPrivacy' | + 'proUpdatePlanDescription' | + 'proUpdatePlanExpireDescription' | + 'processingRefundRequest' | 'rateSessionModalDescription' | + 'refundPlanNonOriginatorApple' | 'remainingCharactersOverTooltip' | 'screenshotTaken' | 'searchMatchesNoneSpecific' | + 'sessionNetworkDataPrice' | 'sessionNetworkDescription' | 'systemInformationDesktop' | 'tooltipAccountIdVisible' | @@ -1224,9 +1367,10 @@ export type TokenSimpleWithArgs = 'updateVersion' | 'updated' | 'urlOpenDescription' | - 'sessionNetworkDataPrice' + 'viaStoreWebsite' | + 'viaStoreWebsiteDescription' -export type TokenPluralWithArgs = +export type TokenPluralWithArgs = 'adminSendingPromotion' | 'clearDataErrorDescription' | 'deleteMessage' | @@ -1375,6 +1519,9 @@ export const simpleDictionaryNoArgs: Record< appIconSelectionTitle: { en: "Icon", }, + appName: { + en: "Session", + }, appNameCalculator: { en: "Calculator", }, @@ -1393,6 +1540,12 @@ export const simpleDictionaryNoArgs: Record< appNameWeather: { en: "Weather", }, + appPro: { + en: "Session Pro", + }, + appProBadge: { + en: "Session Pro Badge", + }, appearanceAutoDarkMode: { en: "Auto Dark Mode", }, @@ -1700,7 +1853,7 @@ export const simpleDictionaryNoArgs: Record< en: "Voice and Video Calls (Beta)", }, callsVoiceAndVideoModalDescription: { - en: "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.", + en: "Your IP is visible to your call partner and a Session Foundation server while using beta calls.", }, callsVoiceAndVideoToggleDescription: { en: "Enables voice and video calls to and from other users.", @@ -1726,6 +1879,9 @@ export const simpleDictionaryNoArgs: Record< cancel: { en: "Cancel", }, + cancelPlan: { + en: "Cancel Plan", + }, change: { en: "Change", }, @@ -1990,6 +2146,12 @@ export const simpleDictionaryNoArgs: Record< creatingCall: { en: "Creating Call", }, + currentPassword: { + en: "Current Password", + }, + currentPlan: { + en: "Current Plan", + }, cut: { en: "Cut", }, @@ -2479,6 +2641,9 @@ export const simpleDictionaryNoArgs: Record< images: { en: "images", }, + important: { + en: "Important", + }, incognitoKeyboard: { en: "Incognito Keyboard", }, @@ -2548,6 +2713,9 @@ export const simpleDictionaryNoArgs: Record< linkPreviewsTurnedOffDescription: { en: "Session must contact linked websites to generate previews of links you send and receive.

You can turn them on in Session's settings.", }, + links: { + en: "Links", + }, loadAccount: { en: "Load Account", }, @@ -2584,9 +2752,15 @@ export const simpleDictionaryNoArgs: Record< lockAppUnlocked: { en: "Session is unlocked", }, + logs: { + en: "Logs", + }, manageMembers: { en: "Manage Members", }, + managePro: { + en: "Manage Pro", + }, max: { en: "Max", }, @@ -2734,9 +2908,18 @@ export const simpleDictionaryNoArgs: Record< modalMessageTooLongTitle: { en: "Message Too Long", }, + networkName: { + en: "Session Network", + }, + newPassword: { + en: "New Password", + }, next: { en: "Next", }, + nextSteps: { + en: "Next Steps", + }, nicknameEnter: { en: "Enter nickname", }, @@ -2953,6 +3136,9 @@ export const simpleDictionaryNoArgs: Record< other: { en: "Other", }, + oxenFoundation: { + en: "Oxen Foundation", + }, password: { en: "Password", }, @@ -3022,12 +3208,12 @@ export const simpleDictionaryNoArgs: Record< passwordStrengthIncludeNumber: { en: "Includes a number", }, - passwordStrengthIncludesLetter: { - en: "Includes a letter", - }, passwordStrengthIncludesLowercase: { en: "Includes a lowercase letter", }, + passwordStrengthIncludesSymbol: { + en: "Includes a symbol", + }, passwordStrengthIncludesUppercase: { en: "Includes a uppercase letter", }, @@ -3157,6 +3343,9 @@ export const simpleDictionaryNoArgs: Record< pinUnpinConversation: { en: "Unpin Conversation", }, + plusLoadsMore: { + en: "Plus Loads More...", + }, preferences: { en: "Preferences", }, @@ -3166,9 +3355,15 @@ export const simpleDictionaryNoArgs: Record< previewNotification: { en: "Preview Notification", }, + pro: { + en: "Pro", + }, proActivated: { en: "Activated", }, + proAllSet: { + en: "You're all set!", + }, proAlreadyPurchased: { en: "You’ve already got", }, @@ -3184,11 +3379,26 @@ export const simpleDictionaryNoArgs: Record< proAnimatedDisplayPictureModalDescription: { en: "users can upload GIFs", }, + proAnimatedDisplayPictures: { + en: "Animated Display Pictures", + }, + proAnimatedDisplayPicturesDescription: { + en: "Set animated GIFs and WebP images as your display picture.", + }, proAnimatedDisplayPicturesNonProModalDescription: { en: "Upload GIFs with", }, proBadge: { - en: "Session Pro Badge", + en: "Pro Badge", + }, + proBadgeVisible: { + en: "Show Session Pro badge to other users", + }, + proBadges: { + en: "Badges", + }, + proBadgesDescription: { + en: "Show your support for Session with an exclusive badge next to your display name.", }, proCallToActionLongerMessages: { en: "Want to send longer messages? Send more text and unlock premium features with Session Pro", @@ -3199,6 +3409,21 @@ export const simpleDictionaryNoArgs: Record< proCallToActionPinnedConversationsMoreThan: { en: "Want more than 5 pins? Organize your chats and unlock premium features with Session Pro", }, + proExpired: { + en: "Expired", + }, + proExpiredDescription: { + en: "Unfortunately, your Pro plan has expired. Renew to keep accessing the exclusive perks and features of Session Pro.", + }, + proExpiringSoon: { + en: "Expiring Soon", + }, + proFaq: { + en: "Pro FAQ", + }, + proFaqDescription: { + en: "Find answers to common questions in the Session FAQ.", + }, proFeatureListAnimatedDisplayPicture: { en: "Upload GIF and WebP display pictures", }, @@ -3214,24 +3439,96 @@ export const simpleDictionaryNoArgs: Record< proFeatureListPinnedConversations: { en: "Pin unlimited conversations", }, + proFeatures: { + en: "Pro Features", + }, proGroupActivated: { en: "Group Activated", }, proGroupActivatedDescription: { en: "This group has expanded capacity! It can support up to 300 members because a group admin has", }, + proImportantDescription: { + en: "Requesting a refund is final. If approved, your Pro plan will be canceled immediately and you will lose access to all Pro features.", + }, proIncreasedAttachmentSizeFeature: { en: "Increased Attachment Size", }, proIncreasedMessageLengthFeature: { en: "Increased Message Length", }, + proLargerGroups: { + en: "Larger Groups", + }, + proLargerGroupsDescription: { + en: "Groups you are an admin in are automatically upgraded to support 300 members.", + }, + proLongerMessages: { + en: "Longer Messages", + }, + proLongerMessagesDescription: { + en: "You can send messages up to 10,000 characters in all conversations.", + }, proMessageInfoFeatures: { en: "This message used the following Session Pro features:", }, + proPlanNotFound: { + en: "Pro Plan Not Found", + }, + proPlanNotFoundDescription: { + en: "No active plan was found for your account. If you believe this is a mistake, please reach out to Session support for assistance.", + }, + proPlanRecover: { + en: "Recover Pro Plan", + }, + proPlanRenew: { + en: "Renew Pro Plan", + }, + proPlanRenewStart: { + en: "Renew your Session Pro plan to start using powerful Session Pro features again.", + }, + proPlanRenewSupport: { + en: "Your Session Pro plan has been renewed! Thank you for supporting the Session Network.", + }, + proPlanRestored: { + en: "Pro Plan Restored", + }, + proPlanRestoredDescription: { + en: "A valid plan for Session Pro was detected and your Pro status has been restored!", + }, + proRefundDescription: { + en: "We’re sorry to see you go. Here's what you need to know before requesting a refund.", + }, + proRefundRequestSessionSupport: { + en: "Your refund request will be handled by Session Support.

Request a refund by hitting the button below and completing the refund request form.

While Session Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume.", + }, + proRefunding: { + en: "Refunding Pro", + }, + proRequestedRefund: { + en: "Refund Requested", + }, proSendMore: { en: "Send more with", }, + proSettings: { + en: "Pro Settings", + }, + proStats: { + en: "Your Pro Stats", + }, + proStatsTooltip: { + en: "Pro stats reflect usage on this device and may appear differently on linked devices", + }, + proSupportDescription: { + en: "Need help with your Pro plan? Submit a request to the support team.", + }, + proUnlimitedPins: { + en: "Unlimited Pins", + }, + proUnlimitedPinsDescription: { + en: "Organize all your chats with unlimited pinned conversations.", + }, proUserProfileModalCallToAction: { en: "Want to get more out of Session? Upgrade to Session Pro for a more powerful messaging experience.", }, @@ -3382,9 +3679,15 @@ export const simpleDictionaryNoArgs: Record< removePasswordModalDescription: { en: "Remove your current password for Session. Locally stored data will be re-encrypted with a randomly generated key, stored on your device.", }, + renew: { + en: "Renew", + }, reply: { en: "Reply", }, + requestRefund: { + en: "Request Refund", + }, resend: { en: "Resend", }, @@ -3484,6 +3787,12 @@ export const simpleDictionaryNoArgs: Record< sessionConversations: { en: "Conversations", }, + sessionDownloadUrl: { + en: "https://getsession.org/download", + }, + sessionFoundation: { + en: "Session Foundation", + }, sessionHelp: { en: "Help", }, @@ -3529,6 +3838,9 @@ export const simpleDictionaryNoArgs: Record< sessionPrivacy: { en: "Privacy", }, + sessionProBeta: { + en: "Session Pro Beta", + }, sessionRecoveryPassword: { en: "Recovery Password", }, @@ -3583,6 +3895,9 @@ export const simpleDictionaryNoArgs: Record< spellChecker: { en: "Spell Checker", }, + stakingRewardPool: { + en: "Staking Reward Pool", + }, stickers: { en: "Stickers", }, @@ -3607,9 +3922,18 @@ export const simpleDictionaryNoArgs: Record< theError: { en: "Error", }, + theReturn: { + en: "Return", + }, themePreview: { en: "Theme Preview", }, + tokenNameLong: { + en: "Session Token", + }, + tokenNameShort: { + en: "SESH", + }, tooltipBlindedIdCommunities: { en: "Blinded IDs are used in communities to reduce spam and increase privacy", }, @@ -3673,6 +3997,12 @@ export const simpleDictionaryNoArgs: Record< updateNewVersion: { en: "A new version of Session is available, tap to update", }, + updatePlan: { + en: "Update Plan", + }, + updatePlanTwo: { + en: "Two ways to update your plan:", + }, updateProfileInformation: { en: "Update Profile Information", }, @@ -3706,6 +4036,12 @@ export const simpleDictionaryNoArgs: Record< urlOpenBrowser: { en: "This will open in your browser.", }, + urlOpenDescriptionAlternative: { + en: "Links will open in your browser.", + }, + usdNameShort: { + en: "USD", + }, useFastMode: { en: "Use Fast Mode", }, @@ -4186,18 +4522,132 @@ export const simpleDictionaryWithArgs: Record< }, notificationsSystem: { en: "{message_count} new messages in {conversation_count} conversations", + }, + onDevice: { + en: "On your {device_type} device", + }, + onDeviceDescription: { + en: "Open this Session account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the Session Pro settings.", }, onboardingBubbleCreatingAnAccountIsEasy: { en: "Creating an account is instant, free, and anonymous {emoji}", }, onboardingBubbleWelcomeToSession: { en: "Welcome to Session {emoji}", + }, + openStoreWebsite: { + en: "Open {platform_store} Website", }, passwordErrorLength: { en: "Password must be between {min} and {max} characters long", + }, + plusLoadsMoreDescription: { + en: "New features coming soon to Pro. Discover what's next on the Pro Roadmap {icon}", + }, + proAllSetDescription: { + en: "Your Session Pro plan was updated! You will be billed when your current Pro plan is automatically renewed on {date}.", + }, + proAutoRenewTime: { + en: "Pro auto-renewing in {time}", + }, + proBadgesSent: { + en: "{count} Pro Badges Sent", + }, + proBilledAnnually: { + en: "{price} Billed Annually", + }, + proBilledMonthly: { + en: "{price} Billed Monthly", + }, + proBilledQuarterly: { + en: "{price} Billed Quarterly", + }, + proDiscountTooltip: { + en: "Your current plan is already discounted by{percent}% of the full Session Pro price.", + }, + proExpiringSoonDescription: { + en: "Your Pro plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of Session Pro.", + }, + proGroupsUpgraded: { + en: "{count} Groups Upgraded", + }, + proLongerMessagesSent: { + en: "{count} Longer Messages Sent", + }, + proPercentOff: { + en: "{percent}% Off", + }, + proPinnedConversations: { + en: "{count} Pinned Conversations", + }, + proPlanActivatedAuto: { + en: "Your Session Pro plan is active!

Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when Pro is next renewed.", + }, + proPlanActivatedAutoShort: { + en: "Your Session Pro plan is active!

Your plan will automatically renew for another {current_plan} on {date}.", + }, + proPlanActivatedNotAuto: { + en: "Your Session Pro plan will expire on {date}.

Update your plan now to ensure uninterrupted access to exclusive Pro features.", + }, + proPlanExpireDate: { + en: "Your Session Pro plan will expire on {date}.", + }, + proPlanPlatformRefund: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, you'll need to use the same {platform_account} to request a refund.", + }, + proPlanPlatformRefundLong: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, your refund request will be processed by Session Support.

Request a refund by hitting the button below and completing the refund request form.

While Session Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume.", + }, + proPlanRenewDesktop: { + en: "Currently, Pro plans can only be purchased and renewed via the {platform_store} or {platform_store} Stores. Because you are using Session Desktop, you're not able to renew your plan here.

Session Pro developers are working hard on alternative payment options to allow users to purchase Pro plans outside of the {platform_store} and {platform_store} Stores. Pro Roadmap", + }, + proPlanRenewDesktopLinked: { + en: "Renew your plan in the Session Pro settings on a linked device with Session installed via the {platform_store} or {platform_store} Store.", + }, + proPlanRenewDesktopStore: { + en: "Renew your plan on the {platform_store} website using the {platform_account} you signed up for Pro with.", + }, + proPlanSignUp: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, you'll need to use your {platform_account} to update your plan.", + }, + proPriceOneMonth: { + en: "1 Month - {monthly_price} / Month", + }, + proPriceThreeMonths: { + en: "3 Months - {monthly_price} / Month", + }, + proPriceTwelveMonths: { + en: "12 Months - {monthly_price} / Month", + }, + proRefundNextSteps: { + en: "{platform_account} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your Pro status change in Session.", + }, + proRefundRequestStorePolicies: { + en: "Your refund request will be handled exclusively by {platform_account} through the {platform_account} website.

Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued.", + }, + proRefundSupport: { + en: "Please contact {platform_account} for further updates on your refund request. Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests.

{platform_store} Refund Support", + }, + proRefundingDescription: { + en: "Refunds for Session Pro plans are handled exclusively by {platform_account} through the {platform_store} Store.

Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued.", + }, + proTosPrivacy: { + en: "By updating, you agree to the Session Pro Terms of Service {icon} and Privacy Policy {icon}", + }, + proUpdatePlanDescription: { + en: "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access.", + }, + proUpdatePlanExpireDescription: { + en: "Your plan will expire on {date}.

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access.", + }, + processingRefundRequest: { + en: "{platform_account} is processing your refund request", }, rateSessionModalDescription: { en: "We're glad you're enjoying Session, if you have a moment, rating us in the {storevariant} helps others discover private, secure messaging!", + }, + refundPlanNonOriginatorApple: { + en: "Because you originally signed up for Session Pro via a different {platform_account}, you'll need to use that {platform_account} to update your plan.", }, remainingCharactersOverTooltip: { en: "Reduce message length by {count}", @@ -4207,6 +4657,9 @@ export const simpleDictionaryWithArgs: Record< }, searchMatchesNoneSpecific: { en: "No results found for {query}", + }, + sessionNetworkDataPrice: { + en: "Price data powered by CoinGecko
Accurate at {date_time}", }, sessionNetworkDescription: { en: "Messages are sent using the Session Network. The network is comprised of nodes incentivized with Session Token, which keeps Session decentralized and secure. Learn More {icon}", @@ -4232,8 +4685,11 @@ export const simpleDictionaryWithArgs: Record< urlOpenDescription: { en: "Are you sure you want to open this URL in your browser?

{url}", }, - sessionNetworkDataPrice: { - en: "Price data powered by CoinGecko
Accurate at {date_time}", + viaStoreWebsite: { + en: "Via the {platform_store} website", + }, + viaStoreWebsiteDescription: { + en: "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website.", }, } as const; diff --git a/run/screenshots/ios/app_disguise.png b/run/screenshots/ios/app_disguise.png index 926271317..7950444a3 100644 --- a/run/screenshots/ios/app_disguise.png +++ b/run/screenshots/ios/app_disguise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74c3f2a2e564e6e78c245d2fd5cf410e930c76672a839dedf85e5177a79ed5ea -size 476984 +oid sha256:f215acd1deb44135705057cc83a0f3997ce999c456783fa3c1209a33b1ca3c48 +size 477016 diff --git a/run/test/specs/app_disguise_icons.spec.ts b/run/test/specs/app_disguise_icons.spec.ts index 5ce75ee21..312c9a5d2 100644 --- a/run/test/specs/app_disguise_icons.spec.ts +++ b/run/test/specs/app_disguise_icons.spec.ts @@ -1,9 +1,9 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { AppearanceMenuItem, SelectAppIcon, UserSettings } from './locators/settings'; -import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { AppDisguisePageScreenshot } from './utils/screenshot_paths'; @@ -22,16 +22,20 @@ bothPlatformsIt({ }); async function appDisguiseIcons(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - await device.clickOnElementAll(new UserSettings(device)); - // Must scroll down to reveal the Appearance menu item - await device.scrollDown(); - await device.clickOnElementAll(new AppearanceMenuItem(device)); - await sleepFor(2000); - // Must scroll down to reveal the app disguise option - await device.scrollDown(); - await device.clickOnElementAll(new SelectAppIcon(device)); - await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); - await closeApp(device); + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.VERIFY.ELEMENT_SCREENSHOT('app disguise icons'), async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); } diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index f01490f7f..15c7d3a84 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -1,7 +1,8 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { androidIt } from '../../types/sessionIt'; +import { TestSteps } from '../../types/allure'; +import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { DisguisedApp } from './locators/external'; import { @@ -18,12 +19,16 @@ import { openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/o import { closeApp } from './utils/open_app'; import { runScriptAndLog } from './utils/utilities'; -// iOS implementation blocked by SES-3809 -androidIt({ +bothPlatformsItSeparate({ title: 'App disguise set icon', risk: 'medium', countOfDevicesNeeded: 1, - testCb: appDisguiseSetIcon, + android: { + testCb: appDisguiseSetIconAndroid, + }, + ios: { + testCb: appDisguiseSetIconIOS, + }, allureSuites: { parent: 'Settings', suite: 'App Disguise', @@ -31,34 +36,80 @@ androidIt({ allureDescription: 'Verifies the alternate icon set on the App Disguise page is applied', }); -async function appDisguiseSetIcon(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - await device.clickOnElementAll(new UserSettings(device)); - // Must scroll down to reveal the Appearance menu item - await device.scrollDown(); - await device.clickOnElementAll(new AppearanceMenuItem(device)); - await sleepFor(2000); - // Must scroll down to reveal the app disguise option - await device.scrollDown(); - await device.clickOnElementAll(new SelectAppIcon(device)); - try { - await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); - await device.checkModalStrings( - englishStrippedStr('appIconAndNameChange').toString(), - englishStrippedStr('appIconAndNameChangeConfirmation').toString() - ); - await device.clickOnElementAll(new CloseAppButton(device)); - await sleepFor(2000); - // Open app library and check for disguised app - await device.swipeFromBottom(); - await device.waitForTextElementToBePresent(new DisguisedApp(device)); - } finally { - // The disguised app must be uninstalled otherwise every following test will fail - await closeApp(device); - await runScriptAndLog( - `${getAdbFullPath()} -s ${device.udid} uninstall network.loki.messenger.qa`, - true - ); - } +async function appDisguiseSetIconIOS(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.USER_ACTIONS.APP_DISGUISE, async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disguise'), async () => { + await device.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'You have changed the icon for “Session”.', + }); + await device.clickOnElementAll({ + strategy: 'accessibility id', + selector: 'OK', + }); + }); + // TODO maybe grab a screenshot of the disguised app and see what you can do with it + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + await runScriptAndLog( + `xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, + true + ); + }); + } + }); +} + +async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.USER_ACTIONS.APP_DISGUISE, async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disgusie'), async () => { + await device.checkModalStrings( + englishStrippedStr('appIconAndNameChange').toString(), + englishStrippedStr('appIconAndNameChangeConfirmation').toString() + ); + }); + await test.step('Verify app icon changed', async () => { + await device.clickOnElementAll(new CloseAppButton(device)); + await sleepFor(2000); + // Open app library and check for disguised app + await device.swipeFromBottom(); + await device.waitForTextElementToBePresent(new DisguisedApp(device)); + }); + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + await runScriptAndLog( + `${getAdbFullPath()} -s ${device.udid} uninstall network.loki.messenger.qa`, + true + ); + }); + } + }); } diff --git a/run/test/specs/check_avatar_color.spec.ts b/run/test/specs/check_avatar_color.spec.ts index aa97d488c..80dbbb1ae 100644 --- a/run/test/specs/check_avatar_color.spec.ts +++ b/run/test/specs/check_avatar_color.spec.ts @@ -4,7 +4,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationSettings } from './locators/conversation'; import { ConversationItem } from './locators/home'; -import { UserSettings } from './locators/settings'; +import { UserAvatar, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { isSameColor } from './utils/check_colour'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -35,7 +35,7 @@ async function avatarColor(platform: SupportedPlatformsType, testInfo: TestInfo) }); await test.step(`Get Alice's avatar color on their device from the Settings screen avatar`, async () => { await alice1.clickOnElementAll(new UserSettings(alice1)); - alice1PixelColor = await alice1.getElementPixelColor(new UserSettings(alice1)); + alice1PixelColor = await alice1.getElementPixelColor(new UserAvatar(alice1)); }); await test.step(`Get Alice's avatar color on bob's device from the Conversation Settings avatar`, async () => { await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts new file mode 100644 index 000000000..5d3dbca26 --- /dev/null +++ b/run/test/specs/community_emoji_react.spec.ts @@ -0,0 +1,61 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_friends } from './state_builder'; +import { joinCommunity } from './utils/join_community'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Send emoji react community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: sendEmojiReactionCommunity, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: 'Verifies that an emoji reaction can be sent and is received in a community', +}); + +async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + const message = `Testing emoji reacts - ${new Date().getTime()} - ${platform}`; + const { + devices: { alice1, bob1 }, + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: false, + testInfo, + }); + }); + await Promise.all( + [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + ); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await bob1.scrollToBottom(); + await bob1.longPressMessage(message); + await bob1.clickOnElementAll(new FirstEmojiReact(bob1)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await bob1.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + await Promise.all( + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)) + ) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index 7c2c4e510..6aca09d0a 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -2,40 +2,23 @@ import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; import { TestSteps } from '../../types/allure'; -import { androidIt, iosIt } from '../../types/sessionIt'; -import { USERNAME } from '../../types/testing'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; -import { newUser } from './utils/create_account'; import { joinCommunity } from './utils/join_community'; -import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// NOTE For some reason Appium takes FOREVER to load the iOS page source of the community on the recipients device -// and as such I haven't found a quick and easy way to verify that they see the new image message -// If this becomes a problem in the future then we can extract the unread count from page source and see it increment after the image gets sent -// But for now we have to trust that the sender seeing 'Sent' also delivers it to others on iOS -// This is also why it's a 1-device test and has its own iosIt definition (and not bothPlatformsItSeparate) - -androidIt({ +bothPlatformsIt({ title: 'Send image to community', risk: 'medium', countOfDevicesNeeded: 2, - testCb: sendImageCommunityAndroid, - allureSuites: { parent: 'Sending Messages', suite: 'Sending Attachments' }, + testCb: sendImageCommunity, + allureSuites: { parent: 'Sending Messages', suite: 'Attachments' }, allureDescription: 'Verifies that an image can be sent and received in a community', }); -iosIt({ - title: 'Send image to community', - risk: 'medium', - countOfDevicesNeeded: 1, - testCb: sendImageCommunityIOS, - allureSuites: { parent: 'Sending Messages', suite: 'Sending Attachments' }, - allureDescription: `Verifies that an image can be sent to a community. - Note that due to Appium's limitations, this test does not verify another device receiving the image.`, -}); - -async function sendImageCommunityAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { @@ -48,45 +31,20 @@ async function sendImageCommunityAndroid(platform: SupportedPlatformsType, testI const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( - [alice1, bob1].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); - }) + [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) ); }); await test.step(TestSteps.SEND.IMAGE, async () => { await alice1.sendImage(testImageMessage, true); }); await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { - await sleepFor(5000); // Give bob some time to receive the message so the test doesn't scroll down too early + await sleepFor(2000); // Give bob some time to receive the image await bob1.scrollToBottom(); - await bob1.trustAttachments(testCommunityName); - await bob1.scrollToBottom(); // Gotta keep scrolling down to make sure we're at the very bottom - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testImageMessage, - }); + await bob1.onAndroid().trustAttachments(testCommunityName); + await bob1.onAndroid().scrollToBottom(); // Trusting attachments scrolls the viewport up a bit so gotta scroll to bottom again + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testImageMessage)); }); - await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); }); } -async function sendImageCommunityIOS(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - return { device }; - }); - const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; - await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { - await joinCommunity(device, testCommunityLink, testCommunityName); - }); - await test.step(TestSteps.SEND.IMAGE, async () => { - await device.sendImage(testImageMessage, true); - }); - - await test.step(TestSteps.SETUP.CLOSE_APP, async () => { - await closeApp(device); - }); -} diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index 90fcb83be..c693693f9 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationItem } from './locators/home'; import { open_Alice2 } from './state_builder'; @@ -24,14 +25,24 @@ bothPlatformsIt({ async function joinCommunityTest(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, alice2 }, - } = await open_Alice2({ platform, testInfo }); + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice2({ platform, testInfo }); + }); const testMessage = `Test message + ${new Date().getTime()}`; - - await joinCommunity(alice1, testCommunityLink, testCommunityName); - await sleepFor(5000); - await alice1.scrollToBottom(); - await alice1.sendMessage(testMessage); - // Has community synced to device 2? - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); - await closeApp(alice1, alice2); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + await sleepFor(5000); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { + await alice1.scrollToBottom(); + await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_SYNCED, async () => { + // Has community synced to device 2? + await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, alice2); + }); } diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index aeabf7bd2..ffaddd951 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -1,7 +1,9 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { checkDisappearingControlMessage } from './utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -23,10 +25,12 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te const { devices: { alice1, bob1 }, prebuilt: { alice, bob }, - } = await open_Alice1_Bob1_friends({ - platform, - testInfo, - focusFriendsConvo: true, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + testInfo, + focusFriendsConvo: true, + }); }); const testMessage = 'Checking disappear after read is working'; @@ -34,41 +38,46 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te // TODO: Consider refactoring DISAPPEARING_TIMES to include ms values const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; // Click conversation options menu (three dots) - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + await setDisappearingMessage( + platform, + alice1, + ['1:1', `Disappear after ${mode} option`, time], + bob1 + ); + }); // Check control message is correct on device 2 - await checkDisappearingControlMessage( - platform, - alice.userName, - bob.userName, - alice1, - bob1, - time, - mode - ); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + await checkDisappearingControlMessage( + platform, + alice.userName, + bob.userName, + alice1, + bob1, + time, + mode + ); + }); // Send message to verify that deletion is working - await alice1.sendMessage(testMessage); - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - ]); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + sentTimestamp = await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { + // NOTE we're only sending a text message, both devices are open, DaS is practically the same as DaR + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + }); // Great success - await closeApp(alice1, bob1); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); } diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index bebfb5226..b19ffb202 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { checkDisappearingControlMessage } from './utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -51,23 +52,17 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te controlMode ); // Send message to verify that deletion is working - await alice1.sendMessage(testMessage); + const sentTimestamp = await alice1.sendMessage(testMessage); // Wait for message to disappear - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - }), - ]); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); // Great success await closeApp(alice1, bob1); diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 8c711e8f5..d46f24bea 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -1,8 +1,10 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -26,44 +28,53 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; const { devices: { alice1, bob1, charlie1 }, prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + }); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); + }); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + // Get correct control message for You setting disappearing messages + const disappearingMessagesSetYou = englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: controlMode }) + .toString(); + // Get correct control message for alice setting disappearing messages + const disappearingMessagesSetControl = englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: alice.userName, time, disappearing_messages_type: controlMode }) + .toString(); + // Check control message is correct on device 1, 2 and 3 + await Promise.all([ + alice1.waitForControlMessageToBePresent(disappearingMessagesSetYou), + bob1.waitForControlMessageToBePresent(disappearingMessagesSetControl), + charlie1.waitForControlMessageToBePresent(disappearingMessagesSetControl), + ]); }); - - await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); - // Get correct control message for You setting disappearing messages - const disappearingMessagesSetYou = englishStrippedStr('disappearingMessagesSetYou') - .withArgs({ time, disappearing_messages_type: controlMode }) - .toString(); - // Get correct control message for alice setting disappearing messages - const disappearingMessagesSetControl = englishStrippedStr('disappearingMessagesSet') - .withArgs({ name: alice.userName, time, disappearing_messages_type: controlMode }) - .toString(); - // Check control message is correct on device 1, 2 and 3 - await Promise.all([ - alice1.waitForControlMessageToBePresent(disappearingMessagesSetYou), - bob1.waitForControlMessageToBePresent(disappearingMessagesSetControl), - charlie1.waitForControlMessageToBePresent(disappearingMessagesSetControl), - ]); // Check for test messages (should be deleted) - await alice1.sendMessage(testMessage); - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - maxWait, - text: testMessage, - preventEarlyDeletion: true, - }) - ) - ); - // Close server and devices - await closeApp(alice1, bob1, charlie1); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testGroupName), async () => { + sentTimestamp = await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); } diff --git a/run/test/specs/disappear_after_send_note_to_self.spec.ts b/run/test/specs/disappear_after_send_note_to_self.spec.ts index 896432dd9..fb7ed8ad5 100644 --- a/run/test/specs/disappear_after_send_note_to_self.spec.ts +++ b/run/test/specs/disappear_after_send_note_to_self.spec.ts @@ -1,7 +1,9 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { PlusButton } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { sleepFor } from './utils'; @@ -23,38 +25,49 @@ bothPlatformsIt({ }); async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); const testMessage = `Testing disappearing messages in Note to Self`; - const alice = await newUser(device, USERNAME.ALICE); const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); // Send message to self to bring up Note to Self conversation - await device.clickOnElementAll(new PlusButton(device)); - await device.clickOnElementAll(new NewMessageOption(device)); - await device.inputText(alice.accountID, new EnterAccountID(device)); - await device.scrollDown(); - await device.clickOnElementAll(new NextButton(device)); - await device.sendMessage('Buy milk'); - // Enable disappearing messages - await setDisappearingMessage(platform, device, [ - 'Note to Self', - 'Disappear after send option', - time, - ]); - await sleepFor(1000); - await device.waitForControlMessageToBePresent( - `You set messages to disappear ${time} after they have been ${controlMode}.` - ); - await device.sendMessage(testMessage); - await device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, + await test.step(TestSteps.OPEN.NTS, async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(alice.accountID, new EnterAccountID(device)); + await device.scrollDown(); + await device.clickOnElementAll(new NextButton(device)); + }); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + // Enable disappearing messages + await setDisappearingMessage(platform, device, [ + 'Note to Self', + 'Disappear after send option', + time, + ]); + await sleepFor(1000); + await device.waitForControlMessageToBePresent( + `You set messages to disappear ${time} after they have been ${controlMode}.` + ); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, 'Note to Self'), async () => { + sentTimestamp = await device.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + await device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + // Great success + await closeApp(device); }); - // Great success - await closeApp(device); } diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 103e77170..1ce067e4b 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -144,21 +144,22 @@ async function disappearingCallMessage1o1Android( maxWait: 5_000, }); await alice1.clickOnElementById('network.loki.messenger.qa:id/endCallButton'); + const callEndTimestamp = Date.now(); // Wait for control message to disappear await Promise.all([ - alice1.hasElementBeenDeleted({ + alice1.hasElementDisappeared({ strategy: 'id', selector: 'network.loki.messenger.qa:id/call_text_view', text: `You called ${bob.userName}`, maxWait, - preventEarlyDeletion: true, + actualStartTime: callEndTimestamp, }), - bob1.hasElementBeenDeleted({ + bob1.hasElementDisappeared({ strategy: 'id', selector: 'network.loki.messenger.qa:id/call_text_view', text: `Missed call from ${alice.userName}`, maxWait, - preventEarlyDeletion: true, + actualStartTime: callEndTimestamp, }), ]); await closeApp(alice1, bob1); diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 12890a2cb..8bf16c480 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -58,11 +58,12 @@ async function disappearingCommunityInviteMessage( await alice1.clickOnElementAll(new CommunityInviteConfirmButton(alice1)); // The community invite process fails silently so we will check if the invite came through first await bob1.waitForTextElementToBePresent(new CommunityInvitation(bob1)); + const communityInviteTimestamp = Date.now(); // Bob already has the convo open so we can start checking for the disappearing message immediately - await bob1.hasElementBeenDeleted({ + await bob1.hasElementDisappeared({ ...new CommunityInvitation(bob1).build(), maxWait, - preventEarlyDeletion: true, + actualStartTime: communityInviteTimestamp, }); // Leave Invite Contacts, Conversation Settings, Community, and open convo with Bob await alice1.navigateBack(); diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index f4eefa5af..657f08802 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { MediaMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -21,7 +22,7 @@ bothPlatformsIt({ // The timing with 30 seconds was a bit tight in terms of the attachment downloading and becoming visible const time = DISAPPEARING_TIMES.ONE_MINUTE; const initialMaxWait = 15_000; // GIFs could be large so give them a bit more time to be found -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 70s plus buffer const timerType = 'Disappear after send option'; async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { @@ -33,16 +34,15 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.sendGIF(); + const sentTimestamp = await alice1.sendGIF(); await bob1.trustAttachments(USERNAME.ALICE); await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index cce7de960..e4d98645c 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -2,8 +2,8 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; -import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -27,28 +27,35 @@ const maxWait = 35_000; // 30s plus buffer async function disappearingImageMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await sleepFor(500); - await alice1.sendImage(testMessage); - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - maxWait, - text: testMessage, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', - maxWait, - preventEarlyDeletion: true, - }), - ]); + const sentTimestamp = await alice1.sendImage(testMessage); + await bob1.trustAttachments(alice.userName); + if (platform === 'ios') { + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + } else { + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + } await closeApp(alice1, bob1); } diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index 396b69454..0fdf1c374 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -6,7 +6,12 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -32,6 +37,7 @@ bothPlatformsItSeparate({ const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const timerType = 'Disappear after read option'; const maxWait = 35_000; // 30s plus buffer +let sentTimestamp: number; async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, testInfo: TestInfo) { const { @@ -43,7 +49,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); }); await test.step(TestSteps.SEND.LINK, async () => { @@ -60,23 +66,21 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t await alice1.deleteText(new MessageInput(alice1)); await alice1.inputText(testLink, new MessageInput(alice1)); await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); - await alice1.clickOnElementAll(new SendButton(alice1)); await alice1.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); // Wait for 30 seconds to disappear await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + device.hasElementDisappeared({ + ...new MessageBody(device, testLink).build(), maxWait, - text: testLink, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -99,7 +103,7 @@ async function disappearingLinkMessage1o1Android( testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { @@ -119,11 +123,16 @@ async function disappearingLinkMessage1o1Android( ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ ...new LinkPreviewMessage(device).build(), maxWait }) + device.hasElementDisappeared({ + ...new LinkPreviewMessage(device).build(), + maxWait, + actualStartTime: sentTimestamp, + }) ) ); }); diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index cf0736a72..67df3bb6f 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -1,7 +1,8 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -23,42 +24,44 @@ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const testMessage = 'Testing disappearing messages for videos'; const initialMaxWait = 20_000; // Downloading the attachment can take a while -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.onIOS().sendVideoiOS(testMessage); - await alice1.onAndroid().sendVideoAndroid(); - await bob1.trustAttachments(USERNAME.ALICE); + let sentTimestamp: number; + if (platform === 'ios') { + sentTimestamp = await alice1.onIOS().sendVideoiOS(testMessage); + } else { + sentTimestamp = await alice1.onAndroid().sendVideoAndroid(); + } + await bob1.trustAttachments(alice.userName); if (platform === 'ios') { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), initialMaxWait, maxWait, - text: testMessage, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); } else if (platform === 'android') { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index 39ee0ffcf..54713a713 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -1,7 +1,8 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { DISAPPEARING_TIMES } from '../../types/testing'; +import { VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -25,31 +26,23 @@ const maxWait = 35_000; // 30s plus buffer async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.sendVoiceMessage(); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }); - await bob1.trustAttachments(USERNAME.ALICE); - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Voice message', - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Voice message', - maxWait, - preventEarlyDeletion: true, - }), - ]); + const sentTimestamp = await alice1.sendVoiceMessage(); + await bob1.trustAttachments(alice.userName); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new VoiceMessage(device).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index a4f8cfa70..c5af419ea 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -18,10 +19,11 @@ bothPlatformsIt({ allureDescription: 'Verifies that a GIF disappears as expected in a group conversation', }); -const time = DISAPPEARING_TIMES.THIRTY_SECONDS; +// The timing with 30 seconds was a bit tight in terms of the attachment downloading and becoming visible +const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const initialMaxWait = 15_000; // Downloading the attachment can take a while -const maxWait = 35_000; // 30s plus buffer +const maxWait = 70_000; // 70s plus buffer async function disappearingGifMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Disappear after sent test'; @@ -35,18 +37,18 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); // Click on attachments button - await alice1.sendGIF(); + const sentTimestamp = await alice1.sendGIF(); + console.log(`the sent timestamp is ${sentTimestamp}`); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 41144319f..5dea27a3f 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -34,36 +35,35 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - // await device1.navigateBack(); - await alice1.sendImage(testMessage); - await Promise.all([ - bob1.onAndroid().trustAttachments(testGroupName), - charlie1.onAndroid().trustAttachments(testGroupName), - ]); + const sentTimestamp = await alice1.sendImage(testMessage); if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), maxWait, - text: testMessage, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); } if (platform === 'android') { - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + await Promise.all([ + alice1.hasElementDisappeared({ + ...new MediaMessage(alice1).build(), + maxWait, + actualStartTime: sentTimestamp, + }), + // Bob and Charlie haven't trusted the message + ...[bob1, charlie1].map(device => + device.hasElementDisappeared({ strategy: 'accessibility id', - selector: 'Media message', + selector: 'Untrusted attachment message', maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) - ) - ); + ), + ]); } await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index 0193909f5..9cc57841e 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -6,7 +6,12 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -28,6 +33,7 @@ const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + let sentTimestamp: number; const testGroupName = 'Testing disappearing messages'; const { devices: { alice1, bob1, charlie1 }, @@ -39,7 +45,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { @@ -63,18 +69,17 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); // Wait for 30 seconds to disappear await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + device.hasElementDisappeared({ + ...new MessageBody(device, testLink).build(), maxWait, - text: testLink, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -82,9 +87,10 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te if (platform === 'android') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new LinkPreviewMessage(device).build(), maxWait, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 31618ff30..1ae36d83e 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -22,7 +23,7 @@ bothPlatformsIt({ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const initialMaxWait = 20_000; // Downloading the attachment can take a while -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testMessage = 'Testing disappearing messages for videos'; @@ -36,32 +37,33 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t testInfo, }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - await alice1.onIOS().sendVideoiOS(testMessage); - await alice1.onAndroid().sendVideoAndroid(); + let sentTimestamp: number; + if (platform === 'ios') { + sentTimestamp = await alice1.sendVideoiOS(testMessage); + } else { + sentTimestamp = await alice1.sendVideoAndroid(); + } await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), maxWait, - text: testMessage, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); } else if (platform === 'android') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_voice.spec.ts b/run/test/specs/group_disappearing_messages_voice.spec.ts index dd7572f4d..83c1bf6a0 100644 --- a/run/test/specs/group_disappearing_messages_voice.spec.ts +++ b/run/test/specs/group_disappearing_messages_voice.spec.ts @@ -32,17 +32,17 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t testInfo, }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - await alice1.sendVoiceMessage(); + const sentTimestamp = await alice1.sendVoiceMessage(); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ strategy: 'accessibility id', selector: 'Voice message', maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_message_delete.spec.ts b/run/test/specs/group_message_delete.spec.ts index c4fff3034..9d80bfd3a 100644 --- a/run/test/specs/group_message_delete.spec.ts +++ b/run/test/specs/group_message_delete.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageLocally } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -24,19 +24,13 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te focusGroupConvo: true, testInfo, }); - const sentMessage = await alice1.sendMessage('Checking local delete functionality'); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + const sentMessage = 'Checking local delete functionality'; + await alice1.sendMessage(sentMessage); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon @@ -52,17 +46,10 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Excellent // Check device 2 and 3 that message is still visible - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_document.spec.ts b/run/test/specs/group_message_document.spec.ts index 5f046e520..edd6378b2 100644 --- a/run/test/specs/group_message_document.spec.ts +++ b/run/test/specs/group_message_document.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { DocumentMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -28,30 +29,19 @@ async function sendDocumentGroupiOS(platform: SupportedPlatformsType, testInfo: focusGroupConvo: true, testInfo, }); - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing documents'; const replyMessage = `Replying to document from ${alice.userName}`; await alice1.sendDocument(); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) + ) + ); await bob1.longPressMessage(testMessage); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1, charlie1); } @@ -76,33 +66,23 @@ async function sendDocumentGroupAndroid(platform: SupportedPlatformsType, testIn charlie1.trustAttachments(testGroupName), ]); // Check document appears in both device 2 and 3's screen - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }), - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }), - ]); - // Reply to document from user B - await bob1.longPress('Document'); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new DocumentMessage(device)) + ) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await sleepFor(1000); + await bob1.longPress(new DocumentMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply from device 2 came through on alice1 and charlie1 - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app and server await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_gif.spec.ts b/run/test/specs/group_message_gif.spec.ts index dc9628311..ae3a5f5b6 100644 --- a/run/test/specs/group_message_gif.spec.ts +++ b/run/test/specs/group_message_gif.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -34,28 +35,21 @@ async function sendGifGroupiOS(platform: SupportedPlatformsType, testInfo: TestI await alice1.sendGIF(); await sleepFor(500); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await bob1.longPress('Media message'); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await sleepFor(1000); + await bob1.longPress(new MediaMessage(bob1)); // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } @@ -78,31 +72,20 @@ async function sendGifGroupAndroid(platform: SupportedPlatformsType, testInfo: T bob1.trustAttachments(testGroupName), charlie1.trustAttachments(testGroupName), ]); - // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - maxWait: 10000, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - maxWait: 10000, - }); - await bob1.longPress('Media message'); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await bob1.longPress(new MediaMessage(bob1)); // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_image.spec.ts b/run/test/specs/group_message_image.spec.ts index 14cd14d76..61fa50522 100644 --- a/run/test/specs/group_message_image.spec.ts +++ b/run/test/specs/group_message_image.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { OutgoingMessageStatusSent } from './locators/conversation'; +import { MediaMessage, MessageBody, OutgoingMessageStatusSent } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -36,35 +36,23 @@ async function sendImageGroupiOS(platform: SupportedPlatformsType, testInfo: Tes ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 50000, }); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, testMessage).build(), + maxWait: 5_000, + }) + ) + ); const replyMessage = await bob1.replyToMessage(alice, testMessage); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - maxWait: 5000, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - maxWait: 5000, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, replyMessage).build(), + maxWait: 5_000, + }) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } @@ -90,34 +78,23 @@ async function sendImageGroupAndroid(platform: SupportedPlatformsType, testInfo: bob1.trustAttachments(testGroupName), charlie1.trustAttachments(testGroupName), ]); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); // Reply to image - user B // Sleep for is waiting for image to load await sleepFor(1000); - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, replyMessage).build(), + maxWait: 5_000, + }) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_link_preview.spec.ts b/run/test/specs/group_message_link_preview.spec.ts index f2eaf381a..f3b73a5e1 100644 --- a/run/test/specs/group_message_link_preview.spec.ts +++ b/run/test/specs/group_message_link_preview.spec.ts @@ -4,7 +4,12 @@ import { testLink } from '../../constants'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -52,30 +57,20 @@ async function sendLinkGroupiOS(platform: SupportedPlatformsType, testInfo: Test await alice1.inputText(testLink, new MessageInput(alice1)); await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testLink)) + ) + ); // Reply to link await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } @@ -113,18 +108,12 @@ async function sendLinkGroupAndroid(platform: SupportedPlatformsType, testInfo: ]); await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); - const replyMessage = await bob1.sendMessage(`${alice.userName} message reply`); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + const replyMessage = `${alice.userName} message reply`; + await bob1.sendMessage(replyMessage); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index d5c7ad9ae..9122cb964 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -1,29 +1,21 @@ import type { TestInfo } from '@playwright/test'; import { longText } from '../../constants'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { OutgoingMessageStatusSent } from './locators/conversation'; -import { ConversationItem } from './locators/home'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send long message to group', risk: 'low', countOfDevicesNeeded: 3, - ios: { - testCb: sendLongMessageGroupiOS, - }, - android: { - testCb: sendLongMessageGroupAndroid, - }, + testCb: sendLongMessageGroup, allureDescription: 'Verifies that a long message can be sent to a group', }); -async function sendLongMessageGroupiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendLongMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Message checks for groups'; - // Sending a long text message - const { devices: { alice1, bob1, charlie1 }, prebuilt: { alice }, @@ -34,82 +26,17 @@ async function sendLongMessageGroupiOS(platform: SupportedPlatformsType, testInf testInfo, }); await alice1.sendMessage(longText); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, longText)) + ) + ); const replyMessage = await bob1.replyToMessage(alice, longText); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Close app - await closeApp(alice1, bob1, charlie1); -} - -async function sendLongMessageGroupAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const testGroupName = 'Message checks for groups'; - - const { - devices: { alice1, bob1, charlie1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, - }); - - // Sending a long text message - await alice1.sendMessage(longText); - await alice1.waitForTextElementToBePresent({ - ...new OutgoingMessageStatusSent(alice1).build(), - maxWait: 50000, - }); - - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }), - ]); - await bob1.longPressMessage(longText); - await bob1.clickOnByAccessibilityID('Reply to message'); - const replyMessage = await bob1.sendMessage(`${alice.userName} message reply`); - // Go out and back into the group to see the last message - await alice1.navigateBack(); - await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Go out and back into the group to see the last message - await charlie1.navigateBack(); - await charlie1.clickOnElementAll(new ConversationItem(charlie1, testGroupName)); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(async device => { + await device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)); + }) + ); // Close app await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_unsend.spec.ts b/run/test/specs/group_message_unsend.spec.ts index fb7420033..3ecd759fa 100644 --- a/run/test/specs/group_message_unsend.spec.ts +++ b/run/test/specs/group_message_unsend.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,19 +25,13 @@ async function unsendMessageGroup(platform: SupportedPlatformsType, testInfo: Te focusGroupConvo: true, testInfo, }); - const sentMessage = await alice1.sendMessage('Checking unsend functionality'); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + const sentMessage = 'Checking unsend functionality'; + await alice1.sendMessage(sentMessage); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon diff --git a/run/test/specs/group_message_video.spec.ts b/run/test/specs/group_message_video.spec.ts index af098bd2f..c3d7af6e8 100644 --- a/run/test/specs/group_message_video.spec.ts +++ b/run/test/specs/group_message_video.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -31,35 +32,19 @@ async function sendVideoGroupiOS(platform: SupportedPlatformsType, testInfo: Tes const testMessage = 'Testing-video-1'; const replyMessage = `Replying to video from ${alice.userName} in ${testGroupName}`; await alice1.sendVideoiOS(testMessage); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) + ) + ); await bob1.longPressMessage(testMessage); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } @@ -106,22 +91,15 @@ async function sendVideoGroupAndroid(platform: SupportedPlatformsType, testInfo: }), ]); // Reply to message on device 2 - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply appears in device 1 and device 3 - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app and server await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index 9c7d342be..2efd9bf0a 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -1,22 +1,18 @@ import type { TestInfo } from '@playwright/test'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody, VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send voice message to group', risk: 'high', countOfDevicesNeeded: 3, - ios: { - testCb: sendVoiceMessageGroupiOS, - }, - android: { - testCb: sendVoiceMessageGroupAndroid, - }, + testCb: sendVoiceMessageGroup, }); -async function sendVoiceMessageGroupiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Message checks for groups'; const { devices: { alice1, bob1, charlie1 }, @@ -30,70 +26,21 @@ async function sendVoiceMessageGroupiOS(platform: SupportedPlatformsType, testIn const replyMessage = `Replying to voice message from ${alice.userName} in ${testGroupName}`; await alice1.sendVoiceMessage(); await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }) - ) + [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); - await bob1.longPress('Voice message'); - await bob1.clickOnByAccessibilityID('Reply to message'); - await bob1.sendMessage(replyMessage); - await Promise.all( - [alice1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }) - ) - ); - // Close server and devices - await closeApp(alice1, bob1, charlie1); -} - -async function sendVoiceMessageGroupAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // open devices - const testGroupName = 'Message checks for groups'; - const { - devices: { alice1, bob1, charlie1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, - }); - const replyMessage = `Replying to voice message from ${alice.userName} in ${testGroupName}`; - // Select voice message button to activate recording state - await alice1.sendVoiceMessage(); - await Promise.all([ - bob1.trustAttachments(testGroupName), - charlie1.trustAttachments(testGroupName), - ]); - // Check device 2 and 3 for voice message from user A await Promise.all( [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }) + device.waitForTextElementToBePresent(new VoiceMessage(device)) ) ); - // Reply to voice message - await bob1.longPress('Voice message'); + await bob1.longPress(new VoiceMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - // Check device 1 and 3 for reply to appear await Promise.all( [alice1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }) + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) ) ); + // Close server and devices await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_reaction.spec.ts b/run/test/specs/group_reaction.spec.ts new file mode 100644 index 000000000..269397caa --- /dev/null +++ b/run/test/specs/group_reaction.spec.ts @@ -0,0 +1,64 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { GROUPNAME } from '../../types/testing'; +import { EmojiReactsCount, EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Send emoji react groups', + risk: 'high', + countOfDevicesNeeded: 3, + testCb: sendEmojiReactionGroup, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: 'Verifies that an emoji reaction can be sent and is received in a group.', +}); + +async function sendEmojiReactionGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const groupName: GROUPNAME = 'Message checks for groups'; + const message = 'Testing emoji reacts'; + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + focusGroupConvo: true, + groupName: groupName, + testInfo, + }); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, groupName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await Promise.all( + [bob1, charlie1].map(async device => { + await device.longPressMessage(message); + await device.clickOnElementAll(new FirstEmojiReact(device)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await device.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }) + ); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + // All clients witness emoji and "2" count + await Promise.all( + [alice1, bob1, charlie1].map(async device => { + await device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)); + await device.waitForTextElementToBePresent(new EmojiReactsCount(device, message, '2')); + }) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); +} diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index ddf444e8f..1981a6331 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -68,8 +68,7 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes await unknown1.navigateBack(); // Leave Message Requests screen (Android) await unknown1.onAndroid().navigateBack(); - await unknown1.selectByText('Conversation list item', group.groupName); - // Check for control message on device 4 + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); await closeApp(alice1, bob1, charlie1, unknown1); } diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index df8a9f46b..3c3a1555a 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -8,7 +8,6 @@ import { ConfirmRemovalButton, GroupMember, ManageMembersMenuItem, - MemberStatus, RemoveMemberButton, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; @@ -49,20 +48,12 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) .toString() ); await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); - if (platform === 'ios') { - // These elements disappear slowly on iOS so we get a chance to check for their presence - await alice1.waitForTextElementToBePresent(new MemberStatus(alice1).build('Pending removal')); - await alice1.hasElementBeenDeleted({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 10_000, - }); - } else { - // These elements disappear immediately on Android so we can't check for their presence - await alice1.verifyElementNotPresent({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 5_000, - }); - } + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + await alice1.verifyElementNotPresent({ + ...new GroupMember(alice1).build(USERNAME.BOB), + maxWait: 5_000, + }); await alice1.navigateBack(); await alice1.navigateBack(); await Promise.all([ diff --git a/run/test/specs/group_tests_mentions.spec.ts b/run/test/specs/group_tests_mentions.spec.ts index 7abf2e39f..e045f88a3 100644 --- a/run/test/specs/group_tests_mentions.spec.ts +++ b/run/test/specs/group_tests_mentions.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,11 +26,7 @@ async function mentionsForGroups(platform: SupportedPlatformsType, testInfo: Tes await alice1.mentionContact(platform, bob); // Check format on User B's device - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: `@You`, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, '@You')); // await device2.findMessageWithBody(`@You`); // Bob to Select User C await bob1.mentionContact(platform, charlie); diff --git a/run/test/specs/invite_a_friend_share.spec.ts b/run/test/specs/invite_a_friend_share.spec.ts index a40a0147d..88ae0d575 100644 --- a/run/test/specs/invite_a_friend_share.spec.ts +++ b/run/test/specs/invite_a_friend_share.spec.ts @@ -3,8 +3,9 @@ import type { TestInfo } from '@playwright/test'; import { IOS_XPATHS } from '../../constants'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { AccountIDDisplay } from './locators/global'; import { PlusButton } from './locators/home'; -import { AccountIDField, InviteAFriendOption, ShareButton } from './locators/start_conversation'; +import { InviteAFriendOption, ShareButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -25,7 +26,7 @@ async function inviteAFriend(platform: SupportedPlatformsType, testInfo: TestInf // Select Invite a Friend await device.clickOnElementAll(new InviteAFriendOption(device)); // Check for presence of Account ID field - await device.waitForTextElementToBePresent(new AccountIDField(device)); + await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); // Tap Share await device.clickOnElementAll(new ShareButton(device)); // defining the "Hey..." message element to retrieve the share message from diff --git a/run/test/specs/linked_device.spec.ts b/run/test/specs/linked_device.spec.ts index 5931d00b1..ac0cfbefc 100644 --- a/run/test/specs/linked_device.spec.ts +++ b/run/test/specs/linked_device.spec.ts @@ -3,7 +3,8 @@ import type { TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UsernameSettings } from './locators'; +import { UsernameDisplay } from './locators'; +import { AccountIDDisplay } from './locators/global'; import { UserSettings } from './locators/settings'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; @@ -29,15 +30,7 @@ async function linkDevice(platform: SupportedPlatformsType, testInfo: TestInfo) // Verify username and session ID match await alice2.clickOnElementAll(new UserSettings(alice2)); // Check username - await alice2.waitForTextElementToBePresent({ - ...new UsernameSettings(alice2).build(), - text: alice.userName, - }); - await alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Account ID', - text: alice.accountID, - }); - + await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, alice.userName)); + await alice2.waitForTextElementToBePresent(new AccountIDDisplay(alice2, alice.accountID)); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index c7ed79837..d288886ab 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -4,6 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal } from './locators'; import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { Contact } from './locators/global'; import { ConversationItem } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice2_Bob1_friends } from './state_builder'; @@ -73,16 +74,8 @@ async function blockUserInConversationOptions( alice2.clickOnElementAll(new BlockedContactsSettings(alice2)), ]); await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }), + alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)), + alice2.waitForTextElementToBePresent(new Contact(alice2, bob.userName)), ]); // Close app await closeApp(alice1, bob1, alice2); diff --git a/run/test/specs/linked_device_change_username.spec.ts b/run/test/specs/linked_device_change_username.spec.ts index eb8ee325b..49e9312e5 100644 --- a/run/test/specs/linked_device_change_username.spec.ts +++ b/run/test/specs/linked_device_change_username.spec.ts @@ -1,29 +1,22 @@ import type { TestInfo } from '@playwright/test'; -import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { TickButton, UsernameInput, UsernameSettings } from './locators'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { ClearInputButton, EditUsernameButton, UsernameDisplay, UsernameInput } from './locators'; import { SaveNameChangeButton, UserSettings } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Change username linked device', risk: 'medium', countOfDevicesNeeded: 2, - ios: { - testCb: changeUsernameLinkediOS, - }, - android: { - testCb: changeUsernameLinkedAndroid, - }, + testCb: changeUsernameLinked, }); -async function changeUsernameLinkediOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function changeUsernameLinked(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, alice2 }, - prebuilt: { alice }, } = await open_Alice2({ platform, testInfo }); const newUsername = 'Alice in chains'; @@ -33,87 +26,13 @@ async function changeUsernameLinkediOS(platform: SupportedPlatformsType, testInf alice2.clickOnElementAll(new UserSettings(alice2)), ]); // select username - await alice1.clickOnElementAll(new UsernameSettings(alice1)); - await alice1.checkModalStrings( - englishStrippedStr('displayNameSet').toString(), - englishStrippedStr('displayNameVisible').toString() - ); + await alice1.clickOnElementAll(new EditUsernameButton(alice1)); // type in new username await sleepFor(100); - await alice1.deleteText(new UsernameInput(alice1)); + await alice1.onIOS().deleteText(new UsernameInput(alice1)); + await alice1.onAndroid().clickOnElementAll(new ClearInputButton(alice1)); await alice1.inputText(newUsername, new UsernameInput(alice1)); await alice1.clickOnElementAll(new SaveNameChangeButton(alice1)); - const username = await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }); - const changedUsername = await alice1.getTextFromElement(username); - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await alice1.closeScreen(); - await alice1.clickOnElementAll(new UserSettings(alice1)); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }), - ]); - await closeApp(alice1, alice2); -} - -async function changeUsernameLinkedAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { - devices: { alice1, alice2 }, - prebuilt: { alice }, - } = await open_Alice2({ platform, testInfo }); - - const newUsername = 'Alice in chains'; - // click on settings/profile avatar - await Promise.all([ - alice1.clickOnElementAll(new UserSettings(alice1)), - alice2.clickOnElementAll(new UserSettings(alice2)), - ]); - // select username - await alice1.clickOnElementAll(new UsernameSettings(alice1)); - // type in new username - await sleepFor(100); - await alice1.deleteText(new UsernameInput(alice1)); - await alice1.inputText(newUsername, new UsernameInput(alice1)); - await alice1.clickOnElementAll(new TickButton(alice1)); - const usernameEl = await alice1.waitForTextElementToBePresent(new UsernameSettings(alice1)); - const changedUsername = await alice1.getTextFromElement(usernameEl); - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - // Get the initial linked username from alice2 - const username2 = await alice2.waitForTextElementToBePresent(new UsernameSettings(alice2)); - let currentLinkedUsername = await alice2.getTextFromElement(username2); - - let currentWait = 0; - const waitPerLoop = 500; - const maxWait = 50000; - - do { - await sleepFor(waitPerLoop); - // Close the screen and navigate back to the User Settings - await alice2.closeScreen(); - await alice2.clickOnElementAll(new UserSettings(alice2)); - currentWait += waitPerLoop; - const linkedUsernameEl = await alice2.waitForTextElementToBePresent( - new UsernameSettings(alice2) - ); - currentLinkedUsername = await alice2.getTextFromElement(linkedUsernameEl); - } while (currentLinkedUsername === alice.userName && currentWait < maxWait); - { - alice2.log('Username not changed yet'); - } + await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, newUsername)); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 88eb0ac8f..40899c26d 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -70,9 +70,7 @@ async function linkedGroupiOS(platform: SupportedPlatformsType, testInfo: TestIn // Wait 5 seconds for name to update await sleepFor(5000); // Check linked device for name change (conversation header name) - await device2.waitForTextElementToBePresent( - new ConversationHeaderName(device2).build(newGroupName) - ); + await device2.waitForTextElementToBePresent(new ConversationHeaderName(device2, newGroupName)); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), @@ -118,9 +116,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType, testInfo: Te // Config message is "Group name is now {group_name}" await device1.waitForControlMessageToBePresent(groupNameNew); // Check linked device for name change (conversation header name) - await device2.waitForTextElementToBePresent( - new ConversationHeaderName(device2).build(newGroupName) - ); + await device2.waitForTextElementToBePresent(new ConversationHeaderName(device2, newGroupName)); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), diff --git a/run/test/specs/linked_device_delete_message.spec.ts b/run/test/specs/linked_device_delete_message.spec.ts index bba534748..1f21683f6 100644 --- a/run/test/specs/linked_device_delete_message.spec.ts +++ b/run/test/specs/linked_device_delete_message.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -22,15 +22,13 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test const testMessage = 'Howdy'; // Send message from user a to user b - const sentMessage = await alice1.sendMessage(testMessage); + await alice1.sendMessage(testMessage); // Check message came through on linked device(3) // Enter conversation with user B on device 3 - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, bob.userName)); - await alice2.selectByText('Conversation list item', bob.userName); - // Find message - await alice2.findMessageWithBody(sentMessage); + await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); // Find message + await alice2.findMessageWithBody(testMessage); // Select message on device 1, long press - await alice1.longPressMessage(sentMessage); + await alice1.longPressMessage(testMessage); // Select delete await alice1.clickOnByAccessibilityID('Delete message'); await alice1.checkModalStrings( @@ -41,18 +39,11 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test // Check linked device for deleted message await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Check device 2 and 3 for no change - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, alice2].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) + ) + ); // Close app await closeApp(alice1, bob1, alice2); } diff --git a/run/test/specs/linked_device_profile_picture_syncs.spec.ts b/run/test/specs/linked_device_profile_picture_syncs.spec.ts index f8f7b83fb..447da819e 100644 --- a/run/test/specs/linked_device_profile_picture_syncs.spec.ts +++ b/run/test/specs/linked_device_profile_picture_syncs.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UserSettings } from './locators/settings'; +import { UserAvatar, UserSettings } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -24,12 +24,13 @@ async function avatarRestored(platform: SupportedPlatformsType, testInfo: TestIn const { devices: { alice1, alice2 }, } = await open_Alice2({ platform, testInfo }); + await alice2.clickOnElementAll(new UserSettings(alice2)); await alice1.uploadProfilePicture(); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await alice2.waitForElementColorMatch(new UserSettings(alice2), expectedPixelHexColor, { - maxWait: 20_000, - elementTimeout: 500, - }); + await alice2.waitForElementColorMatch( + { ...new UserAvatar(alice2).build(), maxWait: 20_000 }, + expectedPixelHexColor + ); }); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 59028e1de..2c77a9faf 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationHeaderName } from './locators/conversation'; +import { ConversationHeaderName, MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; @@ -32,36 +32,18 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo // Check that group has loaded on linked device await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded - await device4.waitForTextElementToBePresent( - new ConversationHeaderName(device4).build(testGroupName) - ); + await device4.waitForTextElementToBePresent(new ConversationHeaderName(device4, testGroupName)); // Check all messages are present await Promise.all([ - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), + device4.waitForTextElementToBePresent(new MessageBody(device4, aliceMessage)), + device4.waitForTextElementToBePresent(new MessageBody(device4, bobMessage)), + device4.waitForTextElementToBePresent(new MessageBody(device4, charlieMessage)), ]); const testMessage2 = 'Checking that message input is working'; await device4.sendMessage(testMessage2); await Promise.all( [device1, device2, device3].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage2, - }) + device.waitForTextElementToBePresent(new MessageBody(device, testMessage2)) ) ); await closeApp(device1, device2, device3, device4); diff --git a/run/test/specs/linked_device_unsend_message.spec.ts b/run/test/specs/linked_device_unsend_message.spec.ts index cf57d807e..3e43eb38a 100644 --- a/run/test/specs/linked_device_unsend_message.spec.ts +++ b/run/test/specs/linked_device_unsend_message.spec.ts @@ -1,10 +1,11 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; -import { ConversationItem } from './locators/home'; +import { DeletedMessage, MessageBody } from './locators/conversation'; +import { ConversationItem, MessageSnippet } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -13,41 +14,88 @@ bothPlatformsIt({ risk: 'medium', testCb: unSendMessageLinkedDevice, countOfDevicesNeeded: 3, + allureSuites: { + parent: 'User Actions', + suite: 'Delete Message', + }, + allureDescription: `Verifies that 'Delete for everyone' in a 1-1 deletes the message in the conversation view and on the home screen for both parties and a linked device.`, }); async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testInfo: TestInfo) { + // we send two messages to make sure deletion works OK + const firstMessage = 'Hello'; + const secondMessage = 'Howdy'; + const { devices: { alice1, alice2, bob1 }, - prebuilt: { bob }, - } = await open_Alice2_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); - - // Send message from user a to user b - const sentMessage = await alice1.sendMessage('Howdy'); - // Check message came through on linked device(3) - // Enter conversation with user B on device 3 - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2)); - await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); - // Find message - await alice2.findMessageWithBody(sentMessage); - // Select message on device 1, long press - await alice1.longPressMessage(sentMessage); - // Select delete - await alice1.clickOnByAccessibilityID('Delete message'); - await alice1.checkModalStrings( - englishStrippedStr('deleteMessage').withArgs({ count: 1 }).toString(), - englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() - ); - // Select delete for everyone - await alice1.clickOnElementAll(new DeleteMessageForEveryone(alice1)); - await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); - await Promise.all( - [alice1, bob1, alice2].map(device => - device.waitForTextElementToBePresent({ - ...new DeletedMessage(device).build(), - maxWait: 5000, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice2_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + }); + + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await alice1.sendMessage(firstMessage); + await alice1.sendMessage(secondMessage); + }); + + // Check message came through on alice2 and bob1 + await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { + await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); + // Find message + await Promise.all( + [bob1, alice2].map(async device => { + await device.findMessageWithBody(firstMessage); + await device.findMessageWithBody(secondMessage); + }) + ); + }); + + // alice1 deletes message for everyone + await test.step(TestSteps.USER_ACTIONS.DELETE_FOR_EVERYONE, async () => { + await alice1.longPressMessage(secondMessage); + await alice1.clickOnByAccessibilityID('Delete message'); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Delete message'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('deleteMessage').withArgs({ count: 1 }).toString(), + englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() + ); + }); + await alice1.clickOnElementAll(new DeleteMessageForEveryone(alice1)); + await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); + }); + + // check the first message is still there, the second message deleted and replaced with deleted message + await test.step(TestSteps.VERIFY.MESSAGE_DELETED('conversation view'), async () => { + await Promise.all( + [alice1, bob1, alice2].map(async device => { + await device.waitForTextElementToBePresent(new MessageBody(device, firstMessage)); + await device.verifyElementNotPresent(new MessageBody(device, secondMessage)); + await device.waitForTextElementToBePresent({ + ...new DeletedMessage(device).build(), + maxWait: 10_000, + }); + await device.back(); }) - ) - ); - // Close app - await closeApp(alice1, bob1, alice2); + ); + }); + + // check home screen snippet now shows first message again + await test.step(TestSteps.VERIFY.MESSAGE_DELETED('home screen'), async () => { + await Promise.all( + [alice1, alice2].map(device => + device.waitForTextElementToBePresent({ + ...new MessageSnippet(device, bob.userName, firstMessage).build(), + maxWait: 5_000, + }) + ) + ); + await bob1.waitForTextElementToBePresent({ + ...new MessageSnippet(bob1, alice.userName, firstMessage).build(), + maxWait: 5_000, + }); + }); + + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, alice2); + }); } diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index f24e1a9b3..960237075 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -23,6 +23,77 @@ export class SendButton extends LocatorsInterface { } } +export class NewVoiceMessageButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'New voice message', + } as const; + } + } +} + +export class MessageBody extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Message body', + text: this.text, + } as const; + } + } +} + +export class VoiceMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Voice message', + } as const; + } + } +} + +export class MediaMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Media message', + } as const; + } + } +} + +export class DocumentMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Document', + } as const; + } + } +} + export class ScrollToBottomButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -103,12 +174,22 @@ export class AttachmentsButton extends LocatorsInterface { } } +// TODO tie this to the message whose status we want to check (similar to EmojiReactsPill) export class OutgoingMessageStatusSent extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: `Message sent status: Sent`, - } as const; + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("network.loki.messenger.qa:id/messageStatusTextView").text("Sent")', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: `Message sent status: Sent`, + } as const; + } } } @@ -130,19 +211,24 @@ export class CallButton extends LocatorsInterface { } export class ConversationHeaderName extends LocatorsInterface { - public build(text?: string) { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Conversation header name', - text, + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))`, + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Conversation header name', - text, + text: this.text, } as const; } } @@ -363,8 +449,8 @@ export class EditNicknameButton extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'edit-profile-icon', + strategy: 'accessibility id', + selector: 'Edit', } as const; case 'ios': return { @@ -432,3 +518,70 @@ export class PreferredDisplayName extends LocatorsInterface { } } } + +export class FirstEmojiReact extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger.qa:id/reaction_1', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: '😂', + } as const; + } + } +} + +// Find the reactions pill underneath a specific message +export class EmojiReactsPill extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private messageText: string + ) { + super(device); + } + + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]`, + } as const; + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]`, + } as const; + } + } +} + +export class EmojiReactsCount extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private messageText: string, + private expectedCount: string = '2' + ) { + super(device); + } + + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${this.expectedCount}"]`, + } as const; + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]`, + } as const; + } + } +} diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index d45a5e650..dc4b1e300 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -1,3 +1,4 @@ +import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { LocatorsInterface } from './index'; export class ModalHeading extends LocatorsInterface { @@ -61,17 +62,24 @@ export class EnableLinkPreviewsModalButton extends LocatorsInterface { } export class Contact extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Contact', + selector: 'pro-badge-text', + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Contact', + text: this.text, } as const; } } @@ -110,3 +118,44 @@ export class DenyPermissionLocator extends LocatorsInterface { } } } + +export class AccountIDDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Account ID', + text: this.text, + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Account ID', + text: this.text, + } as const; + } + } +} + +export class CopyURLButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Copy URL', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Copy URL', + } as const; + } + } +} diff --git a/run/test/specs/locators/global_search.ts b/run/test/specs/locators/global_search.ts index b08aab9f4..3230131fc 100644 --- a/run/test/specs/locators/global_search.ts +++ b/run/test/specs/locators/global_search.ts @@ -7,7 +7,7 @@ export class NoteToSelfOption extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger.qa:id/search_result_title', + selector: 'pro-badge-text', text: 'Note to Self', }; case 'ios': diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 47e76a8c9..f994e7e9f 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -70,8 +70,8 @@ export class UpdateGroupInformation extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'group-name', + strategy: 'accessibility id', + selector: 'Edit', }; case 'ios': { const groupName = this.groupName; @@ -259,7 +259,7 @@ export class GroupMember extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Contact', + selector: 'pro-badge-text', text: `${username}`, } as const; case 'ios': diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index a9e11fad0..df2c090c3 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -40,11 +40,62 @@ export class ConversationItem extends LocatorsInterface { this.text = text; } public build() { - return { - strategy: 'accessibility id', - selector: 'Conversation list item', - text: this.text, - } as const; + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Conversation list item', + text: this.text, + } as const; + } + } +} + +export class MessageRequestItem extends LocatorsInterface { + public text?: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Message request', + text: this.text, + } as const; + } + } +} + +// For identifying a conversation with a specific last message in it +export class MessageSnippet extends LocatorsInterface { + public conversationName: string; + public messageText: string; + + constructor(device: DeviceWrapper, conversationName: string, messageText: string) { + super(device); + this.conversationName = conversationName; + this.messageText = messageText; + } + + public build() { + switch (this.platform) { + case 'ios': + return { + strategy: 'xpath', // For nested elements like this xpath is unfortunately the best choice + selector: `//XCUIElementTypeCell[@name="Conversation list item" and @label="${this.conversationName}"]//XCUIElementTypeStaticText[@name="${this.messageText}"]`, + } as const; + + case 'android': + return { + strategy: 'xpath', + selector: `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${this.conversationName}"]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/snippetTextView" and @text="${this.messageText}"]`, + } as const; + } } } @@ -96,7 +147,10 @@ export class ReviewPromptItsGreatButton extends LocatorsInterface { selector: 'enjoy-session-positive-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'enjoy-session-positive-button', + }; } } } @@ -110,7 +164,10 @@ export class ReviewPromptNeedsWorkButton extends LocatorsInterface { selector: 'enjoy-session-negative-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'enjoy-session-negative-button', + }; } } } @@ -124,7 +181,10 @@ export class ReviewPromptRateAppButton extends LocatorsInterface { selector: 'rate-app-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'rate-app-button', + }; } } } @@ -138,7 +198,10 @@ export class ReviewPromptNotNowButton extends LocatorsInterface { selector: 'not-now-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'not-now-button', + }; } } } @@ -152,7 +215,10 @@ export class ReviewPromptOpenSurveyButton extends LocatorsInterface { selector: 'open-survey-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'open-survey-button', + }; } } } diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 860a106e5..8a61169cc 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -31,7 +31,16 @@ export abstract class LocatorsInterface { export function describeLocator(locator: StrategyExtractionObj & { text?: string }): string { const { strategy, selector, text } = locator; - const base = `${strategy} "${selector}"`; + + // Trim selector if its too long, show beginning and end + const maxSelectorLength = 80; + const halfLength = Math.floor(maxSelectorLength / 2); + const trimmedSelector = + selector.length > maxSelectorLength + ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` + : selector; + + const base = `${strategy} "${trimmedSelector}"`; return text ? `${base} and text "${text}"` : base; } @@ -39,17 +48,6 @@ export function describeLocator(locator: StrategyExtractionObj & { text?: string export abstract class LocatorsInterfaceScreenshot extends LocatorsInterface { abstract screenshotFileName(state?: ElementStates): string; } -// When applying a nickname or username change -export class TickButton extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { strategy: 'accessibility id', selector: 'Set' } as const; - case 'ios': - return { strategy: 'accessibility id', selector: 'Done' } as const; - } - } -} export class ApplyChanges extends LocatorsInterface { public build() { @@ -86,13 +84,13 @@ export class ReadReceiptsButton extends LocatorsInterface { } } -export class ExitUserProfile extends LocatorsInterface { +export class CloseSettings extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'accessibility id', - selector: 'Navigate up', + selector: 'Close', } as const; case 'ios': return { @@ -103,30 +101,54 @@ export class ExitUserProfile extends LocatorsInterface { } } -export class UsernameSettings extends LocatorsInterface { +export class UsernameDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Display name', + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Username', + text: this.text, } as const; } } } -export class UsernameInput extends LocatorsInterface { +export class EditUsernameButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'accessibility id', - selector: 'Enter display name', + selector: 'Edit', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Username', + } as const; + } + } +} + +export class UsernameInput extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'class name', + selector: 'android.widget.EditText', } as const; case 'ios': return { @@ -137,39 +159,40 @@ export class UsernameInput extends LocatorsInterface { } } -export class FirstGif extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class ClearInputButton extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { - strategy: 'xpath', - selector: ANDROID_XPATHS.FIRST_GIF, - }; + strategy: 'id', + selector: 'clear-input-button', + } as const; case 'ios': return { - strategy: 'xpath', - selector: IOS_XPATHS.FIRST_GIF, - }; + strategy: 'id', + selector: 'clear-input-button', + } as const; } } } -export class MediaMessage extends LocatorsInterface { +export class FirstGif extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Media message', + strategy: 'xpath', + selector: ANDROID_XPATHS.FIRST_GIF, }; case 'ios': return { - strategy: 'class name', - selector: 'XCUIElementTypeImage', + strategy: 'xpath', + selector: IOS_XPATHS.FIRST_GIF, }; } } } + export class BlockUser extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -223,7 +246,7 @@ export class JoinCommunityButton extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Join community button', }; case 'ios': @@ -240,7 +263,7 @@ export class CommunityInput extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Community input', }; case 'ios': @@ -360,12 +383,12 @@ export class BlockedContactsSettings extends LocatorsInterface { case 'android': return { strategy: 'accessibility id', - selector: 'Blocked contacts', + selector: 'qa-blocked-contacts-settings-item', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Blocked Contacts', + selector: 'Block contacts - Navigation', }; } } @@ -437,9 +460,8 @@ export class ShareExtensionIcon extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'com.google.android.apps.photos:id/text', - text: `${getAppDisplayName()}`, // Session QA or AQA + strategy: '-android uiautomator', + selector: `new UiSelector().text("${getAppDisplayName()}")`, // Session QA or AQA }; case 'ios': return { diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index e9d741856..ec4c556e8 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -44,6 +44,23 @@ export class UserSettings extends LocatorsInterface { } } +export class UserAvatar extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'User settings', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'User settings', + } as const; + } + } +} + export class RecoveryPasswordMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -117,7 +134,7 @@ export class SaveNameChangeButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Save', + selector: 'update-username-confirm-button', } as const; case 'ios': return { @@ -128,24 +145,6 @@ export class SaveNameChangeButton extends LocatorsInterface { } } -export class BlockedContacts extends LocatorsInterface { - public build(text?: string) { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Contact', - text, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Blocked contacts', - text, - } as const; - } - } -} export class PrivacyMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -165,8 +164,9 @@ export class ConversationsMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Conversations', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))', } as const; case 'ios': return { @@ -182,8 +182,9 @@ export class AppearanceMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Appearance', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))', } as const; case 'ios': return { @@ -194,13 +195,31 @@ export class AppearanceMenuItem extends LocatorsInterface { } } -export class SelectAppIcon extends LocatorsInterface { +export class ClassicLightThemeOption extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger.qa:id/system_settings_app_icon', + selector: 'network.loki.messenger.qa:id/theme_option_classic_light', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Classic Light', + } as const; + } + } +} + +export class SelectAppIcon extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))', } as const; case 'ios': return { @@ -235,8 +254,10 @@ export class AppDisguiseMeetingIcon extends LocatorsInterface { selector: 'MeetingSE option', } as const; case 'ios': - // NOTE see SES-3809 - throw new Error('No locators implemented for iOS'); + return { + strategy: 'accessibility id', + selector: 'Meetings option', + } as const; } } } @@ -277,8 +298,9 @@ export class PathMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'xpath', - selector: `//android.widget.TextView[@text="Path"]`, + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', } as const; case 'ios': return { @@ -288,3 +310,21 @@ export class PathMenuItem extends LocatorsInterface { } } } + +export class VersionNumber extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', + } as const; + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[contains(@name, "Version")]`, + } as const; + } + } +} diff --git a/run/test/specs/locators/start_conversation.ts b/run/test/specs/locators/start_conversation.ts index 2c7b911cd..92042db94 100644 --- a/run/test/specs/locators/start_conversation.ts +++ b/run/test/specs/locators/start_conversation.ts @@ -138,22 +138,6 @@ export class EnterAccountID extends LocatorsInterface { } // INVITE A FRIEND SECTION -export class AccountIDField extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Account ID', - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Account ID', - } as const; - } - } -} export class ShareButton extends LocatorsInterface { public build() { diff --git a/run/test/specs/message_deletion.spec.ts b/run/test/specs/message_deletion.spec.ts index f25df4c07..5297b8740 100644 --- a/run/test/specs/message_deletion.spec.ts +++ b/run/test/specs/message_deletion.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageLocally } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -22,12 +22,9 @@ async function deleteMessage(platform: SupportedPlatformsType, testInfo: TestInf testInfo, }); // send message from User A to User B - const sentMessage = await alice1.sendMessage('Checking local deletetion functionality'); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + const sentMessage = 'Checking local deletetion functionality'; + await alice1.sendMessage(sentMessage); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon @@ -44,11 +41,7 @@ async function deleteMessage(platform: SupportedPlatformsType, testInfo: TestInf await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Device 2 should show no change - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); // Excellent await closeApp(alice1, bob1); diff --git a/run/test/specs/message_document.spec.ts b/run/test/specs/message_document.spec.ts index 60c7b0934..5c7b67a7e 100644 --- a/run/test/specs/message_document.spec.ts +++ b/run/test/specs/message_document.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { DocumentMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -19,30 +20,16 @@ async function sendDocument(platform: SupportedPlatformsType, testInfo: TestInfo focusFriendsConvo: true, testInfo, }); - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing documents'; const replyMessage = `Replying to document from ${alice.userName}`; await alice1.sendDocument(); await bob1.trustAttachments(alice.userName); - // Reply to message - await bob1.onIOS().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); - await bob1.onAndroid().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }); await bob1.onIOS().longPressMessage(testMessage); - await bob1.onAndroid().longPress('Document'); + await bob1.onAndroid().longPress(new DocumentMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_gif.spec.ts b/run/test/specs/message_gif.spec.ts index 1e7e08aed..16a24ecca 100644 --- a/run/test/specs/message_gif.spec.ts +++ b/run/test/specs/message_gif.spec.ts @@ -1,23 +1,18 @@ import type { TestInfo } from '@playwright/test'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; -import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send GIF 1:1', risk: 'medium', countOfDevicesNeeded: 2, - ios: { - testCb: sendGifIos, - }, - android: { - testCb: sendGifAndroid, - }, + testCb: sendGif, }); -async function sendGifIos(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendGif(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, prebuilt: { alice }, @@ -30,51 +25,10 @@ async function sendGifIos(platform: SupportedPlatformsType, testInfo: TestInfo) await alice1.sendGIF(); await bob1.trustAttachments(alice.userName); // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await bob1.longPress('Media message'); - await bob1.clickOnByAccessibilityID('Reply to message'); - await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Close app - await closeApp(alice1, bob1); -} - -async function sendGifAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // Test sending a video - // open devices and server - const { - devices: { alice1, bob1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_friends({ - platform, - focusFriendsConvo: true, - testInfo, - }); - const replyMessage = `Replying to GIF from ${alice.userName}`; - // Click on attachments button - await alice1.sendGIF(); - // Check if the 'Tap to download media' config appears - // Click on config - await bob1.trustAttachments(alice.userName); - // Reply to message - await sleepFor(5000); - await bob1.longPress('Media message'); - // Check reply came through on alice1 + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_image.spec.ts b/run/test/specs/message_image.spec.ts index 22d5ceaa1..17eed7e09 100644 --- a/run/test/specs/message_image.spec.ts +++ b/run/test/specs/message_image.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,19 +27,10 @@ async function sendImage(platform: SupportedPlatformsType, testInfo: TestInfo) { await alice1.sendImage(testMessage); // Trust message on device 2 (bob) await bob1.trustAttachments(alice.userName); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); // Reply to message (on device 2 - Bob) const replyMessage = await bob1.replyToMessage(bob, testMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_link_preview.spec.ts b/run/test/specs/message_link_preview.spec.ts index d3a62b7ca..44d1b3647 100644 --- a/run/test/specs/message_link_preview.spec.ts +++ b/run/test/specs/message_link_preview.spec.ts @@ -4,7 +4,12 @@ import { testLink } from '../../constants'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -51,20 +56,11 @@ async function sendLinkIos(platform: SupportedPlatformsType, testInfo: TestInfo) await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); // Make sure image preview is available in device 2 - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); - + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testLink)); await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index a86772ba6..a4290022d 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -2,6 +2,12 @@ import type { TestInfo } from '@playwright/test'; import { longText } from '../../constants'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -44,11 +50,9 @@ async function sendLongMessageIos(platform: SupportedPlatformsType, testInfo: Te } async function sendLongMessageAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // Sending a long text message - // Open device and server const { devices: { alice1, bob1 }, - prebuilt: { alice }, + prebuilt: { bob }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, @@ -56,14 +60,25 @@ async function sendLongMessageAndroid(platform: SupportedPlatformsType, testInfo }); // Send a long message from User A to User B await alice1.sendMessage(longText); - // Reply to message (User B to User A) - const sentMessage = await bob1.replyToMessage(alice, longText); - // Check reply came through on alice1 - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, + // Bob replies + await bob1.longPressMessage(longText); + await bob1.clickOnByAccessibilityID('Reply to message'); + + const replyMessage = `${bob.userName} replied to ${longText}`; + await bob1.inputText(replyMessage, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + + // This is dumb. The CI doesn't scroll to bottom when ran through Github Actions. + // If you start the emulators on the CI box yourself the test will pass. I have no idea why. + if (process.env.GITHUB_ACTIONS) { + await bob1.scrollToBottom(); + } + + await bob1.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 50000, }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_reaction.spec.ts b/run/test/specs/message_reaction.spec.ts new file mode 100644 index 000000000..bf59aa9ea --- /dev/null +++ b/run/test/specs/message_reaction.spec.ts @@ -0,0 +1,56 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_friends } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Send emoji react 1:1', + risk: 'high', + countOfDevicesNeeded: 2, + testCb: sendEmojiReaction, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: + 'Verifies that an emoji reaction can be sent and is received in a 1-1 conversation.', +}); + +async function sendEmojiReaction(platform: SupportedPlatformsType, testInfo: TestInfo) { + const message = 'Testing emoji reacts'; + const { + devices: { alice1, bob1 }, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: true, + testInfo, + }); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await bob1.longPressMessage(message); + await bob1.clickOnElementAll(new FirstEmojiReact(bob1)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await bob1.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + await Promise.all( + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)) + ) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index 4b64cafec..cdce72623 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppThreeDevices, SupportedPlatformsType } from './utils/open_app'; @@ -45,18 +45,11 @@ async function acceptRequest(platform: SupportedPlatformsType, testInfo: TestInf // Check conversation list for new contact (user A) await device2.navigateBack(); await device2.onAndroid().navigateBack(false); - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }), - ]); + await Promise.all( + [device2, device3].map(device => + device.waitForTextElementToBePresent(new ConversationItem(device, alice.userName)) + ) + ); // Close app await closeApp(device1, device2, device3); } diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index ea24146b8..8a9399f89 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -4,6 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { BlockedContactsSettings } from './locators'; +import { Contact } from './locators/global'; import { MessageRequestsBanner, PlusButton } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; @@ -51,7 +52,10 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn strategy: 'accessibility id', selector: messageRequestsNonePending as AccessibilityId, }), - device3.hasElementBeenDeleted(new MessageRequestsBanner(device3)), + device3.verifyElementNotPresent({ + ...new MessageRequestsBanner(device3).build(), + maxWait: 5_000, + }), ]); const blockedMessage = `"${alice.userName} to ${bob.userName} - shouldn't get through"`; await device1.sendMessage(blockedMessage); @@ -61,33 +65,14 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn await sleepFor(5000); await device2.hasTextElementBeenDeleted('Message body', blockedMessage); // Check that user is on Blocked User list in Settings - - await Promise.all([ - device2.clickOnElementAll(new UserSettings(device2)), - device3.clickOnElementAll(new UserSettings(device3)), - ]); - // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it - await Promise.all([device2.scrollDown(), device3.scrollDown()]); - await Promise.all([ - device2.clickOnElementAll(new ConversationsMenuItem(device2)), - device3.clickOnElementAll(new ConversationsMenuItem(device3)), - ]); - await Promise.all([ - device2.clickOnElementAll(new BlockedContactsSettings(device2)), - device3.clickOnElementAll(new BlockedContactsSettings(device3)), - ]); - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: alice.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: alice.userName, - }), - ]); + await Promise.all( + [device2, device3].map(async device => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new ConversationsMenuItem(device)); + await device.clickOnElementAll(new BlockedContactsSettings(device)); + await device.waitForTextElementToBePresent(new Contact(device, alice.userName)); + }) + ); // Close app await closeApp(device1, device2, device3); } diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index 7e7c87235..442a6032d 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { DeclineMessageRequestButton, DeleteMesssageRequestConfirmation } from './locators'; -import { MessageRequestsBanner, PlusButton } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner, PlusButton } from './locators/home'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; @@ -32,27 +32,13 @@ async function declineRequest(platform: SupportedPlatformsType, testInfo: TestIn await device2.clickOnByAccessibilityID('Message request'); // Check message request appears on linked device (device 3) await device3.clickOnElementAll(new MessageRequestsBanner(device3)); - await device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message request', - }); + await device3.waitForTextElementToBePresent(new MessageRequestItem(device3)); // Click on decline button await device2.clickOnElementAll(new DeclineMessageRequestButton(device2)); - // Are you sure you want to delete message request only for ios - await sleepFor(3000); - // TODO remove onIOS/onAndroid once SES-3846 has been completed - await device2 - .onIOS() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString() - ); - await device2 - .onAndroid() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString() - ); + await device2.checkModalStrings( + englishStrippedStr('delete').toString(), + englishStrippedStr('messageRequestsContactDelete').toString() + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index d8233664e..a67a40d35 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { DeleteMessageRequestButton, DeleteMesssageRequestConfirmation } from './locators'; -import { MessageRequestsBanner } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; @@ -28,21 +28,12 @@ async function deleteRequest(platform: SupportedPlatformsType, testInfo: TestInf await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Swipe left on ios await device2.onIOS().swipeLeftAny('Message request'); - await device2.onAndroid().longPress('Message request'); + await device2.onAndroid().longPress(new MessageRequestItem(device2)); await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); - // TODO remove onIOS/onAndroid once SES-3846 has been completed - await device2 - .onIOS() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString() - ); - await device2 - .onAndroid() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString() - ); + await device2.checkModalStrings( + englishStrippedStr('delete').toString(), + englishStrippedStr('messageRequestsContactDelete').toString() + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); diff --git a/run/test/specs/message_unsend.spec.ts b/run/test/specs/message_unsend.spec.ts index bd52940db..8b90fb94a 100644 --- a/run/test/specs/message_unsend.spec.ts +++ b/run/test/specs/message_unsend.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,14 +25,10 @@ async function unsendMessage(platform: SupportedPlatformsType, testInfo: TestInf const testMessage = 'Checking unsend functionality'; // send message from User A to User B - const sentMessage = await alice1.sendMessage(testMessage); + await alice1.sendMessage(testMessage); // await sleepFor(1000); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); - await alice1.longPressMessage(sentMessage); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); + await alice1.longPressMessage(testMessage); // Select Delete icon await alice1.clickOnByAccessibilityID('Delete message'); // Check modal is correct diff --git a/run/test/specs/message_video.spec.ts b/run/test/specs/message_video.spec.ts index f4d47b978..42b2a0bfb 100644 --- a/run/test/specs/message_video.spec.ts +++ b/run/test/specs/message_video.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -37,17 +38,9 @@ async function sendVideoIos(platform: SupportedPlatformsType, testInfo: TestInfo // User B - Click on untrusted attachment message await bob1.trustAttachments(alice.userName); // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); const replyMessage = await bob1.replyToMessage(alice, testMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } @@ -72,16 +65,11 @@ async function sendVideoAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'id', selector: 'network.loki.messenger.qa:id/play_overlay', }); - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await sleepFor(2000); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index 9e9412a14..4442bdc03 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody, VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,21 +26,12 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test // Select voice message button to activate recording state await alice1.sendVoiceMessage(); await sleepFor(500); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }); - + await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await bob1.trustAttachments(alice.userName); await sleepFor(500); - await bob1.longPress('Voice message'); + await bob1.longPress(new VoiceMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/ons_resolve.spec.ts b/run/test/specs/ons_resolve.spec.ts index d9d270c57..b22edcb2b 100644 --- a/run/test/specs/ons_resolve.spec.ts +++ b/run/test/specs/ons_resolve.spec.ts @@ -42,7 +42,7 @@ async function resolveONS(platform: SupportedPlatformsType, testInfo: TestInfo) }); await test.step(`Verify ONS resolution to pubkey '${expectedPubkey}'`, async () => { await device.waitForTextElementToBePresent({ - ...new ConversationHeaderName(device).build(expectedPubkey), + ...new ConversationHeaderName(device, expectedPubkey).build(), maxWait: 5_000, }); }); diff --git a/run/test/specs/review_negative.spec.ts b/run/test/specs/review_negative.spec.ts index c5bd1439f..63c6c88fc 100644 --- a/run/test/specs/review_negative.spec.ts +++ b/run/test/specs/review_negative.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ReviewPromptNeedsWorkButton, @@ -14,7 +14,7 @@ import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { assertUrlIsReachable } from './utils/utilities'; -androidIt({ +bothPlatformsIt({ title: 'Review prompt negative flow', risk: 'high', countOfDevicesNeeded: 1, @@ -35,7 +35,8 @@ async function reviewPromptNegative(platform: SupportedPlatformsType, testInfo: }); const version = await device.getVersionNumber(); - const url = `https://getsession.org/feedback?platform=${platform}&version=${version}`; + const platformParam = platform === 'ios' ? 'iOS' : 'android'; // we call it ios but the app prints iOS + const url = `https://getsession.org/feedback?platform=${platformParam}&version=${version}`; await test.step(TestSteps.OPEN.PATH, async () => { await device.clickOnElementAll(new PathMenuItem(device)); diff --git a/run/test/specs/review_positive.spec.ts b/run/test/specs/review_positive.spec.ts index 9ed8c8c40..7432b7439 100644 --- a/run/test/specs/review_positive.spec.ts +++ b/run/test/specs/review_positive.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ReviewPromptItsGreatButton, @@ -13,7 +13,7 @@ import { PathMenuItem, UserSettings } from './locators/settings'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -androidIt({ +bothPlatformsIt({ title: 'Review prompt positive flow', risk: 'high', countOfDevicesNeeded: 1, diff --git a/run/test/specs/review_triggers.spec.ts b/run/test/specs/review_triggers.spec.ts index ac2213856..b19e91022 100644 --- a/run/test/specs/review_triggers.spec.ts +++ b/run/test/specs/review_triggers.spec.ts @@ -3,10 +3,12 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { CopyURLButton } from './locators/global'; import { AppearanceMenuItem, + ClassicLightThemeOption, DonationsMenuItem, PathMenuItem, UserSettings, @@ -22,6 +24,7 @@ const reviewTriggers = [ testStepName: 'Open Donations menu item', trigger: async (device: DeviceWrapper) => { await device.clickOnElementAll(new DonationsMenuItem(device)); + await device.clickOnElementAll(new CopyURLButton(device)); // Copy URL dismisses the modal on both platforms }, }, { @@ -30,6 +33,7 @@ const reviewTriggers = [ testStepName: TestSteps.OPEN.PATH, trigger: async (device: DeviceWrapper) => { await device.clickOnElementAll(new PathMenuItem(device)); + await device.back(); }, }, { @@ -39,13 +43,14 @@ const reviewTriggers = [ trigger: async (device: DeviceWrapper) => { await device.scrollDown(); await device.clickOnElementAll(new AppearanceMenuItem(device)); - await device.clickOnElementById('network.loki.messenger.qa:id/theme_option_classic_light'); + await device.clickOnElementAll(new ClassicLightThemeOption(device)); + await device.back(); }, }, ]; for (const { titleSnippet, descriptionSnippet, testStepName, trigger } of reviewTriggers) { - androidIt({ + bothPlatformsIt({ title: `Review prompt ${titleSnippet} trigger`, risk: 'high', countOfDevicesNeeded: 1, @@ -67,7 +72,6 @@ for (const { titleSnippet, descriptionSnippet, testStepName, trigger } of review await device.clickOnElementAll(new UserSettings(device)); await trigger(device); await device.back(); - await device.back(); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('App Review'), async () => { await device.checkModalStrings( diff --git a/run/test/specs/user_actions_block_conversation_list.spec.ts b/run/test/specs/user_actions_block_conversation_list.spec.ts index c8142b3c3..b0134878d 100644 --- a/run/test/specs/user_actions_block_conversation_list.spec.ts +++ b/run/test/specs/user_actions_block_conversation_list.spec.ts @@ -4,7 +4,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BlockedContactsSettings } from './locators'; -import { LongPressBlockOption } from './locators/home'; +import { Contact } from './locators/global'; +import { ConversationItem, LongPressBlockOption } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -42,20 +43,14 @@ async function blockUserInConversationList(platform: SupportedPlatformsType, tes await alice1.clickOnByAccessibilityID('Block'); // Once you block the conversation disappears from the home screen await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: bob.userName, - maxWait: 5000, + ...new ConversationItem(alice1, bob.userName).build(), + maxWait: 5_000, }); await alice1.clickOnElementAll(new UserSettings(alice1)); // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it await alice1.scrollDown(); await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); + await alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/user_actions_block_conversation_options.spec.ts b/run/test/specs/user_actions_block_conversation_options.spec.ts index c8ef5175e..0c44cd58d 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -6,9 +6,10 @@ import { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal, - ExitUserProfile, + CloseSettings, } from './locators'; -import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { BlockedBanner, ConversationSettings, MessageBody } from './locators/conversation'; +import { Contact } from './locators/global'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -73,21 +74,15 @@ async function blockUserInConversationSettings( await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); // Accessibility ID for Blocked Contact not present on iOS - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); + await alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)); await alice1.navigateBack(false); await alice1.navigateBack(false); - await alice1.clickOnElementAll(new ExitUserProfile(alice1)); + await alice1.clickOnElementAll(new CloseSettings(alice1)); // Send message from Blocked User await bob1.sendMessage(blockedMessage); await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: blockedMessage, - maxWait: 5000, + ...new MessageBody(alice1, blockedMessage).build(), + maxWait: 5_000, }); // Close app await closeApp(alice1, bob1); diff --git a/run/test/specs/user_actions_change_profile_picture.spec.ts b/run/test/specs/user_actions_change_profile_picture.spec.ts index 9aa6b4efa..bdca8f7fd 100644 --- a/run/test/specs/user_actions_change_profile_picture.spec.ts +++ b/run/test/specs/user_actions_change_profile_picture.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { UserSettings } from './locators/settings'; +import { UserAvatar } from './locators/settings'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -31,10 +31,10 @@ async function changeProfilePicture(platform: SupportedPlatformsType, testInfo: await device.uploadProfilePicture(); }); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await device.waitForElementColorMatch(new UserSettings(device), expectedPixelHexColor, { - maxWait: 10_000, - elementTimeout: 500, - }); + await device.waitForElementColorMatch( + { ...new UserAvatar(device).build(), maxWait: 10_000 }, + expectedPixelHexColor + ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index 006316fc1..0401da51d 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -1,92 +1,39 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { TickButton, UsernameInput, UsernameSettings } from './locators'; +import { ClearInputButton, EditUsernameButton, UsernameDisplay, UsernameInput } from './locators'; import { SaveNameChangeButton, UserSettings } from './locators/settings'; -import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Change username', risk: 'medium', countOfDevicesNeeded: 1, - ios: { - testCb: changeUsernameiOS, - }, - android: { - testCb: changeUsernameAndroid, + testCb: changeUsername, + allureLinks: { + android: 'SES-4277', }, }); -async function changeUsernameiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function changeUsername(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - const alice = await newUser(device, USERNAME.ALICE); + await newUser(device, USERNAME.ALICE); const newUsername = 'Alice in chains'; // click on settings/profile avatar await device.clickOnElementAll(new UserSettings(device)); // select username - await device.clickOnElementAll(new UsernameSettings(device)); - // New modal pops up + await device.clickOnElementAll(new EditUsernameButton(device)); await device.checkModalStrings( englishStrippedStr('displayNameSet').toString(), englishStrippedStr('displayNameVisible').toString() ); - // type in new username - await sleepFor(100); - await device.deleteText(new UsernameInput(device)); + await device.onIOS().deleteText(new UsernameInput(device)); + await device.onAndroid().clickOnElementAll(new ClearInputButton(device)); await device.inputText(newUsername, new UsernameInput(device)); await device.clickOnElementAll(new SaveNameChangeButton(device)); - const username = await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - }); - const changedUsername = await device.getTextFromElement(username); - device.log('Changed username', changedUsername); - if (changedUsername === newUsername) { - device.log('Username change successful'); - } - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await device.closeScreen(); - await closeApp(device); -} - -async function changeUsernameAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - const alice = await newUser(device, USERNAME.ALICE); - const newUsername = 'Alice in chains'; - // click on settings/profile avatar - await device.clickOnElementAll(new UserSettings(device)); - // select username - await device.clickOnElementAll(new UsernameSettings(device)); - // type in new username - await sleepFor(100); - await device.deleteText(new UsernameInput(device)); - await device.inputText(newUsername, new UsernameInput(device)); - await device.clickOnElementAll(new TickButton(device)); - const username = await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Display name', - text: newUsername, - }); - const changedUsername = await device.getTextFromElement(username); - device.log('Changed username', changedUsername); - if (changedUsername === newUsername) { - device.log('Username change successful'); - } - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await device.closeScreen(); - await device.clickOnElementAll(new UserSettings(device)); - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Display name', - text: newUsername, - }); + await device.waitForTextElementToBePresent(new UsernameDisplay(device, newUsername)); await closeApp(device); } diff --git a/run/test/specs/user_actions_create_contact.spec.ts b/run/test/specs/user_actions_create_contact.spec.ts index 86869ac5c..3dd56e1d1 100644 --- a/run/test/specs/user_actions_create_contact.spec.ts +++ b/run/test/specs/user_actions_create_contact.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { retryMsgSentForBanner } from './utils/create_contact'; import { linkedDevice } from './utils/link_device'; @@ -48,42 +48,11 @@ async function createContact(platform: SupportedPlatformsType, testInfo: TestInf await device1.navigateBack(); await device2.navigateBack(); // Check username has changed from session id on both device 1 and 3 - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: Bob.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: Bob.userName, - }), - ]); - // Check contact is added to contacts list on device 1 and 3 (linked device) - // await Promise.all([ - // device1.clickOnElementAll({ - // strategy: "accessibility id", - // selector: "New conversation button", - // }), - // device3.clickOnElementAll({ - // strategy: "accessibility id", - // selector: "New conversation button", - // }), - // ]); + await Promise.all( + [device1, device3].map(device => + device.waitForTextElementToBePresent(new ConversationItem(device, Bob.userName)) + ) + ); - // NEED CONTACT ACCESSIBILITY ID TO BE ADDED - // await Promise.all([ - // device1.waitForTextElementToBePresent({ - // strategy: "accessibility id", - // selector: "Contacts", - // }), - // device3.waitForTextElementToBePresent({ - // strategy: "accessibility id", - // selector: "Contacts", - // }), - // ]); - - // Wait for tick await closeApp(device1, device2, device3); } diff --git a/run/test/specs/user_actions_delete_contact_ucs.spec.ts b/run/test/specs/user_actions_delete_contact_ucs.spec.ts index 9071297ff..b6e34e506 100644 --- a/run/test/specs/user_actions_delete_contact_ucs.spec.ts +++ b/run/test/specs/user_actions_delete_contact_ucs.spec.ts @@ -8,6 +8,7 @@ import { ConversationSettings, DeleteContactConfirmButton, DeleteContactMenuItem, + MessageBody, } from './locators/conversation'; import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; @@ -78,11 +79,7 @@ async function deleteContactCS(platform: SupportedPlatformsType, testInfo: TestI [alice1, alice2].map(async device => { await device.clickOnElementAll(new MessageRequestsBanner(device)); await device.clickOnByAccessibilityID('Message request'); - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: newMessage, - }); + await device.waitForTextElementToBePresent(new MessageBody(device, newMessage)); }) ); }); diff --git a/run/test/specs/user_actions_hide_recovery_password.spec.ts b/run/test/specs/user_actions_hide_recovery_password.spec.ts index ad9f1ac34..cd6e1fd3c 100644 --- a/run/test/specs/user_actions_hide_recovery_password.spec.ts +++ b/run/test/specs/user_actions_hide_recovery_password.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ContinueButton } from './locators/global'; +import { AccountIDDisplay, ContinueButton } from './locators/global'; import { HideRecoveryPasswordButton, RecoveryPasswordMenuItem, @@ -49,10 +49,7 @@ async function hideRecoveryPassword(platform: SupportedPlatformsType, testInfo: }); // Should be taken back to Settings page after hiding recovery password await device1.onAndroid().scrollUp(); - await device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Account ID', - }); + await device1.waitForTextElementToBePresent(new AccountIDDisplay(device1)); // Check that linked device still has Recovery Password await device2.clickOnElementAll(new UserSettings(device2)); await device2.scrollDown(); diff --git a/run/test/specs/user_actions_read_status.spec.ts b/run/test/specs/user_actions_read_status.spec.ts index 3a7676d1f..8ccc3d1cb 100644 --- a/run/test/specs/user_actions_read_status.spec.ts +++ b/run/test/specs/user_actions_read_status.spec.ts @@ -1,6 +1,8 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; +import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils/index'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,24 +27,12 @@ async function readStatus(platform: SupportedPlatformsType, testInfo: TestInfo) // Go to settings to turn on read status // Device 1 await Promise.all([alice1.turnOnReadReceipts(), bob1.turnOnReadReceipts()]); - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: bob.userName, - }); + await alice1.clickOnElementAll(new ConversationItem(alice1, bob.userName)); // Send message from User A to User B to verify read status is working await alice1.sendMessage(testMessage); await sleepFor(100); - await bob1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); // Check read status on device 1 await alice1.onAndroid().waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index 253ee7a4c..5ba9d51d6 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -26,6 +26,9 @@ bothPlatformsIt({ suite: 'Set Nickname', }, allureDescription: `Verifies that a user can set a nickname for a contact and that it appears correctly in the conversation settings, conversation header and home screen.`, + allureLinks: { + android: 'SES-4424', + }, }); async function setNickname(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts index 2585a19c7..b53fa5dd4 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -5,8 +5,9 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ImageName, ShareExtensionIcon } from './locators'; -import { MessageInput, SendButton } from './locators/conversation'; +import { MessageBody, MessageInput, SendButton } from './locators/conversation'; import { PhotoLibrary } from './locators/external'; +import { Contact } from './locators/global'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { handlePhotosFirstTimeOpen } from './utils/handle_first_open'; @@ -27,6 +28,7 @@ bothPlatformsIt({ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { bob }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return open_Alice1_Bob1_friends({ platform, @@ -58,11 +60,7 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn await alice1.onAndroid().clickOnElementAll(new ImageName(alice1)); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Share' }); await alice1.clickOnElementAll(new ShareExtensionIcon(alice1)); - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Contact', - text: USERNAME.BOB, - }); + await alice1.clickOnElementAll(new Contact(alice1, bob.userName)); await alice1.inputText(testMessage, new MessageInput(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); // Loading screen... @@ -72,11 +70,7 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await bob1.trustAttachments(USERNAME.ALICE); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index f2d002d19..396420332 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockUser, BlockUserConfirmationModal } from './locators'; -import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { BlockedBanner, ConversationSettings, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType } from './utils/open_app'; @@ -49,10 +49,8 @@ async function unblockUser(platform: SupportedPlatformsType, testInfo: TestInfo) // Send message from Blocked User await bob1.sendMessage(blockedMessage); await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: blockedMessage, - maxWait: 5000, + ...new MessageBody(alice1, blockedMessage).build(), + maxWait: 5_000, }); // Now that user is blocked, unblock them await alice1.clickOnElementAll(new BlockedBanner(alice1)); diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 2ccaa6225..eeabca5ad 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -1,3 +1,4 @@ +import * as allure from 'allure-js-commons'; import { execSync } from 'child_process'; import fs from 'fs-extra'; import { glob } from 'glob'; @@ -8,6 +9,7 @@ import { allureResultsDir, GH_PAGES_BASE_URL, } from '../../../../constants/allure'; +import { AllureSuiteConfig } from '../../../../types/allure'; import { SupportedPlatformsType } from '../open_app'; export interface ReportContext { @@ -177,3 +179,49 @@ function getGitCommitSha(): string { function getGitBranch(): string { return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); } +// Handle test-level metadata such as suites, test description or linked issues +export async function setupAllureTestInfo({ + suites, + description, + links, + platform, +}: { + suites?: AllureSuiteConfig; + description?: string; + links?: { + all?: string[] | string; // Bugs affecting both platforms + android?: string[] | string; // Android only - won't appear in iOS reports + ios?: string[] | string; // iOS only - won't appear in Android reports + }; + platform?: 'android' | 'ios'; +}) { + // Handle suites + if (suites) { + await allure.parentSuite(suites.parent); + if ('suite' in suites) { + await allure.suite(suites.suite); + } + } + + // Handle description + if (description) { + await allure.description(description); + } + + // Handle links (only process if platform is provided) + if (links && platform) { + const allLinks = links.all ? (Array.isArray(links.all) ? links.all : [links.all]) : []; + + const platformLinks = links[platform] + ? Array.isArray(links[platform]) + ? links[platform] + : [links[platform]] + : []; + + const combinedLinks = [...allLinks, ...platformLinks]; + + for (const jiraKey of combinedLinks) { + await allure.link(`https://optf.atlassian.net/browse/${jiraKey}`, jiraKey, 'issue'); + } + } +} diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 5d835f95d..87bf203bd 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -28,6 +28,7 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { 'appium:processArguments': { env: { debugDisappearingMessageDurations: 'true', + communityPollLimit: 5, }, }, // "appium:isHeadless": true, diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index cfccd1237..853ad31fa 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -2,7 +2,8 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; -import { ContinueButton } from '../locators/global'; +import { CloseSettings } from '../locators'; +import { AccountIDDisplay, ContinueButton } from '../locators/global'; import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locators/onboarding'; import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; import { UserSettings } from '../locators/settings'; @@ -56,7 +57,8 @@ export async function newUser( // Exit Modal await device.navigateBack(false); await device.clickOnElementAll(new UserSettings(device)); - const accountID = await device.grabTextFromAccessibilityId('Account ID'); - await device.closeScreen(false); + const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); + const accountID = await device.getTextFromElement(el); + await device.clickOnElementAll(new CloseSettings(device)); return { userName, accountID, recoveryPhrase }; } diff --git a/run/test/specs/utils/create_contact.ts b/run/test/specs/utils/create_contact.ts index 16cdbeb51..eeba536f2 100644 --- a/run/test/specs/utils/create_contact.ts +++ b/run/test/specs/utils/create_contact.ts @@ -1,6 +1,7 @@ import { runOnlyOnIOS, sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; +import { MessageBody } from '../locators/conversation'; import { MessageRequestsBanner } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; @@ -28,11 +29,7 @@ export const newContact = async ( // TO DO - ADD BACK IN ONCE IOS AND ANDROID HAS FIXED THIS ISSUE // const messageRequestsAccepted = englishStrippedStr('messageRequestsAccepted').toString(); // await device1.onAndroid().waitForControlMessageToBePresent(messageRequestsAccepted); - await device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await device1.waitForTextElementToBePresent(new MessageBody(device1, replyMessage)); console.info(`${sender.userName} and ${receiver.userName} are now contacts`); return { sender, receiver, device1, device2 }; }; diff --git a/run/test/specs/utils/create_group.ts b/run/test/specs/utils/create_group.ts index a2a29b575..3e0eae2f3 100644 --- a/run/test/specs/utils/create_group.ts +++ b/run/test/specs/utils/create_group.ts @@ -1,6 +1,7 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { Group, GROUPNAME, User } from '../../../types/testing'; +import { MessageBody } from '../locators/conversation'; import { Contact } from '../locators/global'; import { CreateGroupButton, GroupNameInput } from '../locators/groups'; import { ConversationItem, PlusButton } from '../locators/home'; @@ -84,45 +85,24 @@ export const createGroup = async ( // Send message from User A to group to verify all working await device1.sendMessage(aliceMessage); // Did the other devices receive alice's message? - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - ]); + await Promise.all( + [device2, device3].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, aliceMessage)) + ) + ); // Send message from User B to group await device2.sendMessage(bobMessage); - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - ]); + await Promise.all( + [device1, device3].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, bobMessage)) + ) + ); // Send message to User C to group await device3.sendMessage(charlieMessage); - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), - ]); + await Promise.all( + [device1, device2].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, charlieMessage)) + ) + ); return { userName, userOne, userTwo, userThree }; }; diff --git a/run/test/specs/utils/disappearing_control_messages.ts b/run/test/specs/utils/disappearing_control_messages.ts index 28e796b38..ea2b22a4b 100644 --- a/run/test/specs/utils/disappearing_control_messages.ts +++ b/run/test/specs/utils/disappearing_control_messages.ts @@ -3,6 +3,7 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { DisappearActions, DISAPPEARING_TIMES } from '../../../types/testing'; +import { ConversationItem } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; export const checkDisappearingControlMessage = async ( @@ -47,11 +48,7 @@ export const checkDisappearingControlMessage = async ( } // Check if control messages are syncing from both user A and user B if (linkedDevice) { - await linkedDevice.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: userNameB, - }); + await linkedDevice.clickOnElementAll(new ConversationItem(linkedDevice, userNameB)); await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetYou); await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetBob); } diff --git a/run/test/specs/utils/get_account_id.ts b/run/test/specs/utils/get_account_id.ts index bd1e5a0c3..4820217e2 100644 --- a/run/test/specs/utils/get_account_id.ts +++ b/run/test/specs/utils/get_account_id.ts @@ -1,18 +1,6 @@ -import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; import { SupportedPlatformsType } from './open_app'; -export const saveSessionIdIos = async (device: DeviceWrapper) => { - const selector = await device.grabTextFromAccessibilityId('Session ID generated'); - return selector; -}; - -export const getAccountId = async (device: DeviceWrapper) => { - const AccountId = await device.grabTextFromAccessibilityId('Account ID'); - - return AccountId; -}; - export function sortByPubkey(...users: Array) { return [...users] .sort((a, b) => a.accountID.localeCompare(b.accountID)) diff --git a/run/test/specs/utils/handle_first_open.ts b/run/test/specs/utils/handle_first_open.ts index 46cf09be5..3844220b7 100644 --- a/run/test/specs/utils/handle_first_open.ts +++ b/run/test/specs/utils/handle_first_open.ts @@ -35,12 +35,22 @@ export async function handlePhotosFirstTimeOpen(device: DeviceWrapper) { } } // On Android, the Photos app shows a sign-in prompt the first time it's opened that needs to be dismissed + // I've seen two different kinds of sign in buttons on the same set of emulators if (device.isAndroid()) { - const signInButton = await device.doesElementExist({ + let signInButton = null; + signInButton = await device.doesElementExist({ strategy: 'id', selector: 'com.google.android.apps.photos:id/sign_in_button', - maxWait: 2_000, + maxWait: 1_000, }); + + if (!signInButton) { + signInButton = await device.doesElementExist({ + strategy: '-android uiautomator', + selector: 'new UiSelector().text("Sign in")', + maxWait: 1_000, + }); + } if (!signInButton) { device.log(`Photos app opened without a sign-in prompt, proceeding`); } else { diff --git a/run/test/specs/utils/index.ts b/run/test/specs/utils/index.ts index cbc92334b..66e353af7 100644 --- a/run/test/specs/utils/index.ts +++ b/run/test/specs/utils/index.ts @@ -1,13 +1,5 @@ import { clickOnCoordinates } from './click_by_coordinates'; -import { getAccountId, saveSessionIdIos } from './get_account_id'; import { runOnlyOnAndroid, runOnlyOnIOS } from './run_on'; import { sleepFor } from './sleep_for'; -export { - sleepFor, - saveSessionIdIos, - getAccountId, - runOnlyOnIOS, - runOnlyOnAndroid, - clickOnCoordinates, -}; +export { sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; diff --git a/run/test/specs/utils/join_community.ts b/run/test/specs/utils/join_community.ts index 1b845d6b4..a18102732 100644 --- a/run/test/specs/utils/join_community.ts +++ b/run/test/specs/utils/join_community.ts @@ -13,7 +13,5 @@ export const joinCommunity = async ( await device.clickOnElementAll(new JoinCommunityOption(device)); await device.inputText(communityLink, new CommunityInput(device)); await device.clickOnElementAll(new JoinCommunityButton(device)); - await device.waitForTextElementToBePresent( - new ConversationHeaderName(device).build(communityName) - ); + await device.waitForTextElementToBePresent(new ConversationHeaderName(device, communityName)); }; diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index 291c8dd33..ac427e820 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -268,6 +268,10 @@ const openAndroidApp = async ( `); await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put global animator_duration_scale 0 `); + await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put global show_first_crash_dialog 0 + `); + await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put secure anr_show_background 0 +`); await wrappedDevice.createSession(capabilities); diff --git a/run/test/specs/utils/screenshot_paths.ts b/run/test/specs/utils/screenshot_paths.ts index f03aaed68..1bff8b8e5 100644 --- a/run/test/specs/utils/screenshot_paths.ts +++ b/run/test/specs/utils/screenshot_paths.ts @@ -1,9 +1,7 @@ import path from 'path'; -import { PageName } from '../../../types/testing'; import { EmptyLandingPage } from '../locators/home'; import { AppDisguisePage } from '../locators/settings'; -import { SupportedPlatformsType } from './open_app'; // Extends locator classes with baseline screenshot paths for visual regression testing // If a locator appears in multiple states, a state argument must be provided to screenshotFileName() @@ -15,12 +13,6 @@ export class EmptyLandingPageScreenshot extends EmptyLandingPage { } } -export class BrowserPageScreenshot { - public screenshotFileName(platform: SupportedPlatformsType, pageName: PageName): string { - return path.join('run', 'screenshots', platform, `browser_${pageName}.png`); - } -} - export class AppDisguisePageScreenshot extends AppDisguisePage { public screenshotFileName(): string { return path.join('run', 'screenshots', this.platform, 'app_disguise.png'); diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 70c458622..f949acce4 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -5,11 +5,8 @@ import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; -import { PageName } from '../../../types/testing'; import { LocatorsInterfaceScreenshot } from '../locators'; -import { SupportedPlatformsType } from './open_app'; -import { BrowserPageScreenshot } from './screenshot_paths'; -import { cropScreenshot, getDiffDirectory, saveImage } from './utilities'; +import { getDiffDirectory } from './utilities'; type Attachment = { name: string; @@ -114,7 +111,8 @@ export async function verifyElementScreenshot< contentType: 'image/png', }, ]); - throw new Error(`The images do not match. The diff has been saved to ${diffImagePath}`); + console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`); + throw new Error(`The UI doesn't match expected appearance`); } // Cleanup of element screenshot file on success @@ -128,46 +126,3 @@ export async function verifyElementScreenshot< } } } - -export async function verifyPageScreenshot( - platform: SupportedPlatformsType, - device: DeviceWrapper, - page: PageName -): Promise { - // Create file path for the diff image (if doesn't exist) - const diffsDir = getDiffDirectory(); - // Capture screenshot - const screenshotBase64 = await device.getScreenshot(); - const screenshotBuffer = Buffer.from(screenshotBase64, 'base64'); - // Need to crop screenshot to cut out time - const croppedBuf = await cropScreenshot(device, screenshotBuffer); - // Create file path for the screenshot - const screenshotName = await saveImage(croppedBuf, diffsDir, 'screenshot'); - // Create custom file path for the baseline screenshot - const baselinePath = new BrowserPageScreenshot().screenshotFileName(platform, page); - fs.mkdirSync(path.dirname(baselinePath), { recursive: true }); - - if (!fs.existsSync(baselinePath)) { - fs.writeFileSync(baselinePath, croppedBuf); - console.warn(`No baseline existed – created new baseline for "${page}" at:\n ${baselinePath}`); - return; - } - // otherwise compare against the existing baseline - const { equal, diffImage } = await looksSame(croppedBuf, baselinePath, { - createDiffImage: true, - }); - - if (!equal) { - const diffImagePath = await saveImage(diffImage, diffsDir, 'diff'); - throw new Error(`Screenshot did not match baseline. Diff saved to:\n ${diffImagePath}`); - } - // Cleanup of element screenshot file on success - try { - fs.unlinkSync(screenshotName); - console.log('Temporary screenshot deleted successfully'); - } catch (err) { - if (err instanceof Error) { - console.error(`Error deleting file: ${err.message}`); - } - } -} diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index f1c6e06ea..4241abd73 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -3,6 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { CloseSettings } from './locators'; import { CallButton, NotificationSettings, NotificationSwitch } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils/index'; @@ -77,7 +78,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo ); } }); - await alice1.closeScreen(); + await alice1.clickOnElementAll(new CloseSettings(alice1)); // Alice tries again, call is created but Bob still hasn't enabled their calls perms so this will fail await test.step(TestSteps.CALLS.INITIATE_CALL(alice.userName), async () => { await alice1.clickOnElementAll(new CallButton(alice1)); @@ -118,7 +119,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo Retrying won't help - use a real device where you can manually enable the permission.` ); } - await bob1.closeScreen(); + await bob1.clickOnElementAll(new CloseSettings(bob1)); await alice1.clickOnElementAll(new CallButton(alice1)); await bob1.clickOnByAccessibilityID('Answer call'); await Promise.all( diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 0c637fade..869eab614 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -12,6 +12,7 @@ import * as sinon from 'sinon'; import { ChangeProfilePictureButton, + CloseSettings, describeLocator, DownloadMediaButton, FirstGif, @@ -30,18 +31,22 @@ import { import { englishStrippedStr } from '../localizer/englishStrippedStr'; import { AttachmentsButton, + MessageBody, MessageInput, + NewVoiceMessageButton, OutgoingMessageStatusSent, ScrollToBottomButton, SendButton, } from '../test/specs/locators/conversation'; -import { ModalDescription, ModalHeading } from '../test/specs/locators/global'; -import { PlusButton } from '../test/specs/locators/home'; +import { Contact, ModalDescription, ModalHeading } from '../test/specs/locators/global'; +import { ConversationItem, PlusButton } from '../test/specs/locators/home'; import { LoadingAnimation } from '../test/specs/locators/onboarding'; import { PrivacyMenuItem, SaveProfilePictureButton, + UserAvatar, UserSettings, + VersionNumber, } from '../test/specs/locators/settings'; import { EnterAccountID, @@ -87,7 +92,6 @@ export class DeviceWrapper { private readonly device: AndroidUiautomator2Driver | XCUITestDriver; public readonly udid: string; private deviceIdentity: string = ''; - private version: string | null = null; private testInfo: TestInfo; constructor( @@ -306,10 +310,11 @@ export class DeviceWrapper { // ELEMENT INTERACTION - // Heal a broken locator by finding potential fuzzy matches in the page source and log it for a permanent fix. + // Heal a broken locator by finding potential fuzzy matches with text as first-class criteria private async findBestMatch( strategy: Strategy, - selector: string + selector: string, + text?: string ): Promise<{ strategy: Strategy; selector: string } | null> { const pageSource = await this.getPageSource(); const threshold = 0.35; // 0.0 = exact, 1.0 = match anything @@ -324,6 +329,11 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; + const blacklist = [ + { from: 'Voice message', to: 'New voice message' }, + { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, + ]; + // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores // Strip any known prefix patterns first const stripPrefix = (selector: string) => { @@ -361,25 +371,88 @@ export class DeviceWrapper { const results = fuse.search(stripPrefix(selector)); - if (results.length > 0 && results[0].score !== undefined && results[0].score < threshold) { - const match = results[0].item; - const confidence = ((1 - results[0].score) * 100).toFixed(2); + // Evaluate each candidate with BOTH selector similarity AND text content + for (const result of results) { + if (result.score === undefined || result.score >= threshold) continue; + + const match = result.item; + const selectorConfidence = ((1 - result.score) * 100).toFixed(2); + + const isBlacklisted = blacklist.some( + pair => + (selector.includes(pair.from) && match.originalSelector.includes(pair.to)) || + (selector.includes(pair.to) && match.originalSelector.includes(pair.from)) + ); - // Sometimes the element is just not on screen yet - proceed. + // Don't heal blacklisted pairs + if (isBlacklisted) { + continue; + } + + // Sometimes the element is just not on screen yet - skip if (match.strategy === strategy && match.originalSelector === selector) { - return null; + continue; + } + + // If we need text validation, check it as part of matching criteria + let textMatches = true; + if (text) { + try { + const healedElements = await (this.toShared().findElements( + match.strategy, + match.originalSelector + ) as Promise>); + + if (healedElements && healedElements.length > 0) { + textMatches = false; // Assume no match until proven otherwise + for (const element of healedElements) { + try { + const elementText = await this.getTextFromElement(element); + if (elementText.includes(text)) { + textMatches = true; + break; + } + } catch (e) { + continue; // Skip elements that can't provide text + } + } + } else { + textMatches = false; // No elements found + } + } catch (e) { + textMatches = false; // Error getting elements + } + } + + // Only accept candidates that pass BOTH selector similarity AND text content + if (textMatches) { + // Check if we've already logged this exact healing + // Only log new healing signatures + const healingSignature = `${strategy} "${selector}" ➡ ${match.strategy} "${match.originalSelector}"`; + const alreadyLogged = this.testInfo.annotations.some( + a => a.type === 'healed' && a.description?.includes(healingSignature) + ); + + if (!alreadyLogged) { + this.log( + `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.originalSelector}" (${selectorConfidence}% match)` + ); + this.testInfo.annotations.push({ + type: 'healed', + description: ` ${healingSignature} (${selectorConfidence}% match)`, + }); + } + + return { + strategy: match.strategy, + selector: match.originalSelector, + }; + } else if (text) { + // Log why this candidate was rejected + this.log( + `Candidate ${match.strategy} "${match.originalSelector}" (${selectorConfidence}% match) rejected: missing text "${text}"` + ); } - this.log( - `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.selector}" (${confidence}% match)` - ); - this.testInfo.annotations.push({ - type: 'healed', - description: ` ${strategy} "${selector}" ➡ ${match.strategy} "${match.selector}" (${confidence}% match)`, - }); - return { - strategy: match.strategy, - selector: match.originalSelector, - }; } return null; @@ -387,9 +460,14 @@ export class DeviceWrapper { /** * Finds element with self-healing for id/accessibility id strategies. - * Throws if not found even after healing attempt. + * @param skipHealing - Disable self-healing for this call + * @throws If element not found even after healing attempt. */ - public async findElement(strategy: Strategy, selector: string): Promise { + public async findElement( + strategy: Strategy, + selector: string, + skipHealing = false + ): Promise { try { return await (this.toShared().findElement( strategy, @@ -398,16 +476,16 @@ export class DeviceWrapper { } catch (originalError) { // Only try healing for id/accessibility id selectors // In the future we can think about extracting values from XPATH etc. - if (strategy !== 'accessibility id' && strategy !== 'id') { + if (skipHealing || (strategy !== 'accessibility id' && strategy !== 'id')) { throw originalError; } - const best = await this.findBestMatch(strategy, selector); + const healed = await this.findBestMatch(strategy, selector); - if (best) { + if (healed) { return await (this.toShared().findElement( - best.strategy, - best.selector + healed.strategy, + healed.selector ) as Promise); } @@ -417,11 +495,15 @@ export class DeviceWrapper { /** * Finds elements with self-healing for id/accessibility id strategies. + * @param skipHealing - Disable self-healing for this call + * @param expectedText - If provided, validates that at least one healed element contains this text * Returns empty array if not found. */ public async findElements( strategy: Strategy, - selector: string + selector: string, + skipHealing = false, + expectedText?: string ): Promise> { const elements = await (this.toShared().findElements(strategy, selector) as Promise< Array @@ -429,13 +511,13 @@ export class DeviceWrapper { if (elements && elements.length > 0) { return elements; } + // Only try healing for id/accessibility id selectors - // In the future we can think about extracting values from XPATH etc. - if (strategy !== 'accessibility id' && strategy !== 'id') { + if (skipHealing || (strategy !== 'accessibility id' && strategy !== 'id')) { return []; } - const healed = await this.findBestMatch(strategy, selector); + const healed = await this.findBestMatch(strategy, selector, expectedText); if (healed) { return ( @@ -449,14 +531,16 @@ export class DeviceWrapper { } /** - * Attempts to click an element using a primary locator, and if not found, falls back to a secondary locator. + * Attempts to find an element using a primary locator, and if not found, falls back to a secondary locator. * This is useful for supporting UI transitions (e.g., between legacy and Compose Android screens) where - * the same UI element may have different locators depending context. + * the same UI element may have different locators depending on context. * - * @param primaryLocator - The first locator to try (e.g., new Compose locator or legacy locator). - * @param fallbackLocator - The locator to try if the primary is not found. + * @param primaryLocator - The first locator to try (e.g., new Compose locator). + * @param fallbackLocator - The locator to try if the primary is not found (e.g., legacy locator). * @param maxWait - Maximum wait time in milliseconds for each locator (default: 3000). - * @throws If neither locator is found. + * @returns The found element, which can be used for clicking, text extraction, or other operations. + * @throws If neither locator finds an element within the timeout period. + * */ public async findWithFallback( primaryLocator: LocatorsInterface | StrategyExtractionObj, @@ -467,21 +551,27 @@ export class DeviceWrapper { primaryLocator instanceof LocatorsInterface ? primaryLocator.build() : primaryLocator; const fallback = fallbackLocator instanceof LocatorsInterface ? fallbackLocator.build() : fallbackLocator; - let found = await this.doesElementExist({ ...primary, maxWait }); - if (found) { - await this.clickOnElementAll(primary); - return found; - } - console.warn( - `[findWithFallback] Could not find primary locator with '${primary.strategy}', falling back on '${fallback.strategy}'` - ); - found = await this.doesElementExist({ ...fallback, maxWait }); - if (found) { - await this.clickOnElementAll(fallback); - return found; + const primaryDescription = describeLocator(primary); + const fallbackDescription = describeLocator(fallback); + + try { + return await this.waitForTextElementToBePresent({ ...primary, maxWait, skipHealing: true }); + } catch (primaryError) { + console.warn( + `[findWithFallback] Could not find element with ${primaryDescription}, falling back to ${fallbackDescription}` + ); + + try { + return await this.waitForTextElementToBePresent({ + ...fallback, + maxWait, + skipHealing: true, + }); + } catch (fallbackError) { + throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`); + } } - throw new Error(`[findWithFallback] Could not find primary or fallback locator`); } public async longClick(element: AppiumNextElementType, durationMs: number) { @@ -589,39 +679,39 @@ export class DeviceWrapper { } await this.click(el.ELEMENT); } - // TODO update this function to handle new locator logic - public async longPress(accessibilityId: AccessibilityId, text?: string) { - const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: accessibilityId, - text, - }); - if (!el) { - throw new Error(`longPress: Could not find accessibilityId: ${accessibilityId}`); - } - await this.longClick(el, 2000); + public async longPress( + args: { text?: string; duration?: number } & (LocatorsInterface | StrategyExtractionObj) + ): Promise { + const { text, duration = 2000 } = args; + const locator = args instanceof LocatorsInterface ? args.build() : args; + // Merge text if provided + const finalLocator = text ? { ...locator, text } : locator; + + const el = await this.waitForTextElementToBePresent({ ...finalLocator }); + + await this.longClick(el, duration); } public async longPressMessage(textToLookFor: string) { - const maxRetries = 3; - let attempt = 0; - let success = false; + const truncatedText = + textToLookFor.length > 50 ? textToLookFor.substring(0, 50) + '...' : textToLookFor; - while (attempt < maxRetries && !success) { - try { + const result = await this.pollUntil( + async () => { + // Find the message const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: textToLookFor, - maxWait: 1000, + ...new MessageBody(this, textToLookFor).build(), + maxWait: 1_000, }); + if (!el) { - throw new Error( - `longPress on message: ${textToLookFor} unsuccessful, couldn't find message` - ); + return { success: false, error: `Couldn't find message: ${truncatedText}` }; } - await this.longClick(el, 4000); + // Attempt long click + await this.longClick(el, 2000); + + // Check if context menu appeared const longPressSuccess = await this.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Reply to message', @@ -630,21 +720,22 @@ export class DeviceWrapper { if (longPressSuccess) { this.log('LongClick successful'); - success = true; // Exit the loop if successful - } else { - throw new Error(`longPress on message: ${textToLookFor} unsuccessful`); - } - } catch (error) { - attempt++; - if (attempt >= maxRetries) { - throw new Error( - `Longpress on message: ${textToLookFor} unsuccessful after ${maxRetries} attempts, ${(error as Error).toString()}` - ); + return { success: true, data: el }; } - this.log(`Longpress attempt ${attempt} failed. Retrying...`); - await sleepFor(1000); + + return { + success: false, + error: `Long press didn't show context menu for: ${truncatedText}`, + }; + }, + { + maxWait: 10_000, + pollInterval: 1000, + onAttempt: attempt => this.log(`Longpress attempt ${attempt}...`), } - } + ); + + return result; // or whatever you want to do with it } public async longPressConversation(userName: string) { @@ -654,11 +745,7 @@ export class DeviceWrapper { while (attempt < maxRetries && !success) { try { - const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: userName, - }); + const el = await this.waitForTextElementToBePresent(new ConversationItem(this, userName)); if (!el) { throw new Error( @@ -868,9 +955,6 @@ export class DeviceWrapper { const matching = await this.findAsync(elements, async e => { const text = await this.getTextFromElement(e); const isPartialMatch = text && text.toLowerCase().includes(textToLookFor.toLowerCase()); - if (isPartialMatch) { - this.info(`Text found to include ${textToLookFor}`); - } return Boolean(isPartialMatch); }); @@ -907,12 +991,7 @@ export class DeviceWrapper { } public async findMessageWithBody(textToLookFor: string): Promise { - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: textToLookFor, - }); - + await this.waitForTextElementToBePresent(new MessageBody(this, textToLookFor)); const message = await this.findMatchingTextAndAccessibilityId('Message body', textToLookFor); return message; } @@ -938,7 +1017,7 @@ export class DeviceWrapper { // Find all candidate elements matching the locator const elements = await this.findElements(locator.strategy, locator.selector); this.info( - `[matchAndTapImage] Found ${elements.length} elements for ${locator.strategy} "${locator.selector}"` + `[matchAndTapImage] Starting image matching: ${elements.length} elements with ${locator.strategy} "${locator.selector}"` ); // Load the reference image buffer from disk @@ -951,8 +1030,7 @@ export class DeviceWrapper { } | null = null; // Iterate over each candidate element - for (const [i, el] of elements.entries()) { - this.info(`[matchAndTapImage] Processing element ${i + 1}/${elements.length}`); + for (const el of elements) { // Take a screenshot of the element const base64 = await this.getElementScreenshot(el.ELEMENT); @@ -985,7 +1063,6 @@ export class DeviceWrapper { const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { threshold, }); - this.info(`[matchAndTapImage] Match score for element ${i + 1}: ${score.toFixed(4)}`); /** * Matching is done on a resized reference image to account for device pixel density. @@ -1018,7 +1095,7 @@ export class DeviceWrapper { // If earlyMatch is enabled and the score is high enough, tap immediately if (earlyMatch && score >= earlyMatchThreshold) { this.info( - `[matchAndTapImage] Tapping first match with ${(score * 100).toFixed(2)}% confidence` + `[matchAndTapImage] Tapping first high-confidence match (${(score * 100).toFixed(2)}%)` ); await clickOnCoordinates(this, center); return; @@ -1026,21 +1103,17 @@ export class DeviceWrapper { // Otherwise, keep track of the best match so far if (!bestMatch || score > bestMatch.score) { bestMatch = { center, score }; - this.info(`[matchAndTapImage] New best match: ${(score * 100).toFixed(2)}% confidence`); } - } catch (err) { - // If matching fails for this element, log and continue to the next - this.warn( - `[matchAndTapImage] Matching failed for element ${i + 1}:`, - err instanceof Error ? err.message : err - ); + } catch { + continue; // No match in this element, try next } } // If no good match was found, throw an error if (!bestMatch) { - throw new Error( + console.log( `[matchAndTapImage] No matching image found among ${elements.length} elements for ${locator.strategy} "${locator.selector}"` ); + throw new Error('Unable to find the expected UI element on screen'); } // Tap the best match found this.info( @@ -1064,14 +1137,18 @@ export class DeviceWrapper { args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) ): Promise { try { - return await this.waitForTextElementToBePresent(args); + const locatorArgs = + args instanceof LocatorsInterface + ? { ...args.build(), text: args.text, maxWait: args.maxWait, skipHealing: true } + : { ...args, skipHealing: true }; + return await this.waitForTextElementToBePresent(locatorArgs); } catch { return null; } } /** - * Ensures an element is not present on the screen at the end of the wait time. + * Ensures an element is not visible on the screen at the end of the wait time. * This allows any transitions to complete and tolerates some UI flakiness. * Unlike hasElementBeenDeleted, this doesn't require the element to exist first. * @@ -1099,13 +1176,23 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text: args.text }); if (element) { - throw new Error( - `Element with ${description} is present after ${maxWait}ms when it should not be` - ); + // Elements can disappear in the GUI but still be present in the DOM + try { + const isVisible = await this.isVisible(element.ELEMENT); + if (isVisible) { + throw new Error( + `Element with ${description} is visible after ${maxWait}ms when it should not be` + ); + } + // Element exists but not visible - that's okay + this.log(`Element with ${description} exists but is not visible`); + } catch (e) { + // Stale element or other error - element is gone, that's okay + this.log(`Element with ${description} is not present (stale reference)`); + } + } else { + this.log(`Verified no element with ${description} is present`); } - - // Element not found - success! - this.log(`Verified no element with ${description} is present`); } /** @@ -1115,21 +1202,16 @@ export class DeviceWrapper { * @param args.text - Optional text content to match within elements of the same type * @param args.initialMaxWait - Time to wait for element to initially appear (defaults to 10_000ms) * @param args.maxWait - Time to wait for deletion AFTER element is found (defaults to 30_000ms) - * @param args.preventEarlyDeletion - If true, throws an error if the element disappears too early (% of maxWait) * * @throws Error if: * - The element is never found within initialMaxWait * - The element still exists after maxWait - * - The element disappears suspiciously early (if preventEarlyDeletion is true) - * Note: For checks where you just need to ensure an element - * is not present (regardless of prior existence), use verifyElementNotPresent() instead. */ public async hasElementBeenDeleted( args: { text?: string; initialMaxWait?: number; maxWait?: number; - preventEarlyDeletion?: boolean; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1144,7 +1226,6 @@ export class DeviceWrapper { // Phase 1: Wait for element to appear this.log(`Waiting for element with ${description} to be deleted...`); await this.waitForElementToAppear(locator, initialMaxWait, text); - const foundTime = Date.now(); this.log(`Element with ${description} has been found, now waiting for deletion`); // Phase 2: Wait for element to disappear @@ -1153,21 +1234,67 @@ export class DeviceWrapper { // Always calculate total time for logging const totalTime = (Date.now() - functionStartTime) / 1000; - // Sometimes an early deletion could indicate a bug (e.g. Disappearing Messages) - if (args.preventEarlyDeletion) { - const deletionPhaseTime = (Date.now() - foundTime) / 1000; - const expectedTotalTime = maxWait / 1000; - const minAcceptableTotalTimeFactor = 0.65; // Catches egregiously early deletions but still enough leeway for sending/trusting/receiving - const minAcceptableTotalTime = expectedTotalTime * minAcceptableTotalTimeFactor; - - if (totalTime < minAcceptableTotalTime) { - throw new Error( - `Element with ${description} disappeared suspiciously early: ${totalTime.toFixed(1)}s total ` + - `(found after ${((foundTime - functionStartTime) / 1000).toFixed(1)}s, ` + - `deleted after ${deletionPhaseTime.toFixed(1)}s). ` + - `Expected ~${expectedTotalTime}s total.` - ); - } + this.log( + `Element with ${description} has been deleted after ${totalTime.toFixed(1)}s total time` + ); + } + + /** + * Waits for an element to disappear from screen (using the Disappearing Messages feature) + * + * @param args - Locator (LocatorsInterface or StrategyExtractionObj) with optional properties + * @param args.actualStartTime - Timestamp of when the timer should be considered to have started. + * @param args.text - Optional text content to match within elements of the same type + * @param args.initialMaxWait - Time to wait for element to initially appear (defaults to 10_000ms) + * @param args.maxWait - Time to wait for deletion AFTER element is found (defaults to 30_000ms) + * + * @throws Error if: + * - The element is never found within initialMaxWait + * - The element still exists after maxWait + * - The element disappears suspiciously early + * + * Note: + * - If you want to ensure an element was present but disappeared (without Disappearing Messages logic), use hasElementBeenDeleted(). + * - If you want to ensure an element is no longer visible (regardless of prior existence), use verifyElementNotPresent(). + */ + public async hasElementDisappeared( + args: { + actualStartTime: number; + text?: string; + initialMaxWait?: number; + maxWait?: number; + } & (LocatorsInterface | StrategyExtractionObj) + ): Promise { + const locator = args instanceof LocatorsInterface ? args.build() : args; + const text = args.text; + const initialMaxWait = args.initialMaxWait ?? 10_000; + const maxWait = args.maxWait ?? 30_000; + + const description = describeLocator({ ...locator, text: args.text }); + + // Phase 1: Wait for element to appear + this.log(`Waiting for element with ${description} to be deleted...`); + await this.waitForElementToAppear(locator, initialMaxWait, text); + const foundTime = Date.now(); + this.log(`Element with ${description} has been found, now waiting for deletion`); + + // Phase 2: Wait for element to disappear + await this.waitForElementToDisappear(locator, maxWait, text); + + // Elements should not disappear too early (could be a DM bug) + const totalTime = (Date.now() - args.actualStartTime) / 1000; + const deletionPhaseTime = (Date.now() - foundTime) / 1000; + const expectedTotalTime = maxWait / 1000; + const minAcceptableTotalTimeFactor = 0.65; // Catches egregiously early deletions but still enough leeway for sending/trusting/receiving + const minAcceptableTotalTime = expectedTotalTime * minAcceptableTotalTimeFactor; + + if (totalTime < minAcceptableTotalTime) { + throw new Error( + `Element with ${description} disappeared suspiciously early: ${totalTime.toFixed(1)}s total ` + + `(found after ${((foundTime - args.actualStartTime) / 1000).toFixed(1)}s, ` + + `deleted after ${deletionPhaseTime.toFixed(1)}s). ` + + `Expected ~${expectedTotalTime}s total.` + ); } this.log( @@ -1182,18 +1309,26 @@ export class DeviceWrapper { timeout: number, text?: string ): Promise { - const start = Date.now(); - - while (Date.now() - start < timeout) { - const element = await this.findElementQuietly(locator, text); - if (element) return; - await sleepFor(100); - } - const desc = describeLocator({ ...locator, text }); - throw new Error( - `Element with ${desc} was never found within ${timeout}ms - cannot verify deletion of non-existent element` + + const element = await this.pollUntil( + async () => { + const foundElement = await this.findElementQuietly(locator, text); + return foundElement + ? { success: true, data: foundElement } + : { success: false, error: `Element with ${desc} not found` }; + }, + { + maxWait: timeout, + pollInterval: 100, + } ); + + if (!element) { + throw new Error( + `Element with ${desc} was never found within ${timeout}ms - cannot verify deletion of non-existent element` + ); + } } /** @@ -1252,7 +1387,7 @@ export class DeviceWrapper { } /** - * Find an element without throwing errors or logging. + * Find an element without throwing errors, logging or healing. */ private async findElementQuietly( locator: StrategyExtractionObj, @@ -1260,7 +1395,7 @@ export class DeviceWrapper { ): Promise { try { if (text) { - const elements = await this.findElements(locator.strategy, locator.selector); + const elements = await this.findElements(locator.strategy, locator.selector, true); for (const element of elements) { const elementText = await this.getText(element.ELEMENT); if (elementText && elementText.toLowerCase() === text.toLowerCase()) { @@ -1269,7 +1404,7 @@ export class DeviceWrapper { } return null; } - return await this.findElement(locator.strategy, locator.selector); + return await this.findElement(locator.strategy, locator.selector, true); } catch { return null; } @@ -1314,52 +1449,80 @@ export class DeviceWrapper { } // WAIT FOR FUNCTIONS + /** + * Waits for an element to be present with optional text matching and self-healing. + * Continuously polls for maxWait seconds, then attempts healing as last resort if not found. + * + * @param args - Locator and options (text, maxWait, skipHealing) + * @returns Promise resolving to the found element + * @throws If element not found + */ public async waitForTextElementToBePresent( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) + args: { text?: string; maxWait?: number; skipHealing?: boolean } & ( + | LocatorsInterface + | StrategyExtractionObj + ) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; - const { text, maxWait = 30_000 } = args; + + // Prefer text from args (if passed directly), otherwise check locator + const text = args.text ?? ('text' in locator ? locator.text : undefined); + + const { maxWait = 30_000 } = args; + const skipHealing = 'skipHealing' in args ? (args.skipHealing ?? false) : false; const description = describeLocator({ ...locator, text }); this.log(`Waiting for element with ${description} to be present`); + // Helper function to find element with or without healing + const tryFindElement = async (allowHealing: boolean): Promise => { + try { + if (text) { + const els = await this.findElements( + locator.strategy, + locator.selector, + !allowHealing, + text + ); + return await this.findMatchingTextInElementArray(els, text); + } + return await this.findElement(locator.strategy, locator.selector, !allowHealing); + } catch (err) { + return null; + } + }; + const result = await this.pollUntil( async () => { - try { - let element: AppiumNextElementType | null = null; - - if (text) { - const els = await this.findElements(locator.strategy, locator.selector); - element = await this.findMatchingTextInElementArray(els, text); - } else { - element = await this.findElement(locator.strategy, locator.selector); - } - - return element - ? { success: true, data: element } - : { success: false, error: `Element with ${description} not found` }; - } catch (err) { - return { - success: false, - error: `Element with ${description} not found`, - }; - } + const element = await tryFindElement(false); // No healing during polling + return element + ? { success: true, data: element } + : { success: false, error: `Element with ${description} not found` }; }, { maxWait } - ); - - if (!result) { - throw new Error(`Waited too long for element with ${description}`); - } - + ).catch(async originalError => { + // If healing is disabled, re-throw original error + if (skipHealing) throw originalError; + + // One attempt at healing after polling fails + const element = await tryFindElement(true); + if (element) { + // Healing succeeded + return element; + } + // Healing failed, re-throw original error + throw originalError; + }); + // Element was found as-is this.log(`Element with ${description} has been found`); - return result; + return result!; // Result must exist if we reached this point } public async waitForControlMessageToBePresent( text: string, maxWait = 15000 ): Promise { + this.log(`Waiting for control message "${text}" to be present`); const result = await this.pollUntil( async () => { try { @@ -1368,11 +1531,11 @@ export class DeviceWrapper { return element ? { success: true, data: element } - : { success: false, error: `Control message with text "${text}" not found` }; + : { success: false, error: `Control message "${text}" not found` }; } catch (err) { return { success: false, - error: err instanceof Error ? err.message : String(err), + error: `Control message "${text}" not found`, }; } }, @@ -1380,7 +1543,7 @@ export class DeviceWrapper { ); if (!result) { - throw new Error(`Control message "${text}" not found after ${maxWait}ms`); + throw new Error(`Waited too long for control message "${text}"`); } this.log(`Control message "${text}" has been found`); @@ -1462,69 +1625,39 @@ export class DeviceWrapper { } } while (elapsed < maxWait); // Log the error with details but only throw generic error so that they get grouped in the report - this.error(`${lastError} after ${attempt} attempts (${elapsed}ms)`); + this.log(`${lastError} after ${attempt} attempts (${elapsed}ms)`); throw new Error(lastError || 'Polling failed'); } - - /** - * Wait for an element to meet a specific condition - */ - async waitForElementCondition( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), - checkElement: (element: AppiumNextElementType) => Promise>, - options: { - maxWait?: number; - elementTimeout?: number; - } = {} - ): Promise { - const { elementTimeout = 500 } = options; - return this.pollUntil(async () => { - try { - // Convert to StrategyExtractionObj if needed - const locator = args instanceof LocatorsInterface ? args.build() : args; - - // Create new args with short timeout for polling - const pollArgs = { - ...locator, - text: args.text, - maxWait: elementTimeout, // Short timeout for each poll attempt - }; - - const element = await this.waitForTextElementToBePresent(pollArgs); - return await checkElement(element); - } catch (error) { - return { - success: false, - error: `Element not found: ${error instanceof Error ? error.message : String(error)}`, - }; - } - }, options); - } /** * Waits for an element's screenshot to match a specific color. * - * @param args - Element locator + * @param args - Element locator with optional text and maxWait * @param expectedColor - Hex color code (e.g., '04cbfe') - * @param options - Optional timeouts: maxWait (total) and elementTimeout (per check) * @throws If color doesn't match within timeout - * */ + public async waitForElementColorMatch( args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), - expectedColor: string, - options: { - maxWait?: number; - elementTimeout?: number; - } = {} + expectedColor: string ): Promise { - await this.waitForElementCondition( - args, - async (element): Promise => { - // Capture screenshot of the element as base64 + const locator = args instanceof LocatorsInterface ? args.build() : args; + const description = describeLocator({ ...locator, text: args.text }); + + this.log(`Waiting for ${description} to have color #${expectedColor}`); + + await this.pollUntil( + async () => { + const element = await this.findElementQuietly(locator, args.text); + + if (!element) { + return { + success: false, + error: `Element not found`, + }; + } + const base64 = await this.getElementScreenshot(element.ELEMENT); - // Extract the middle pixel color from the screenshot const actualColor = await parseDataImage(base64); - // Compare colors using the standard color matcher const matches = isSameColor(expectedColor, actualColor); return { @@ -1534,13 +1667,14 @@ export class DeviceWrapper { : `Color mismatch: expected #${expectedColor}, got #${actualColor}`, }; }, - options + { + maxWait: args.maxWait, // Will use default from pollUntil if undefined + } ); } - // UTILITY FUNCTIONS - public async sendMessage(message: string) { + public async sendMessage(message: string): Promise { await this.inputText(message, new MessageInput(this)); // Click send @@ -1550,32 +1684,14 @@ export class DeviceWrapper { throw new Error('Send button not found: Need to restart iOS emulator: Known issue'); } // Might need to scroll down if the message is too long - await this.scrollToBottom(); + // await this.scrollToBottom(); TODO temporarily disabling this to verify // Wait for tick await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), maxWait: 50000, }); - - return message; - } - - public async waitForSentConfirmation() { - let pendingStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Sending', - }); - const failedStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Failed to send', - }); - if (pendingStatus || failedStatus) { - await sleepFor(100); - pendingStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Sending', - }); - } + const sentTimestamp = Date.now(); + return sentTimestamp; } public async sendNewMessage(user: Pick, message: string) { @@ -1608,17 +1724,7 @@ export class DeviceWrapper { public async sendMessageTo(sender: User, receiver: Group | User) { const message = `${sender.userName} to ${receiver.userName}`; - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: receiver.userName, - }); - await sleepFor(100); - await this.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: receiver.userName, - }); + await this.clickOnElementAll(new ConversationItem(this, receiver.userName)); this.log(`${sender.userName} + " sent message to ${receiver.userName}`); await this.sendMessage(message); this.log(`Message received by ${receiver.userName} from ${sender.userName}`); @@ -1641,7 +1747,8 @@ export class DeviceWrapper { } // Select 'Reply' option // Send message - const replyMessage = await this.sendMessage(`${user.userName} replied to ${body}`); + const replyMessage = `${user.userName} replied to ${body}`; + await this.sendMessage(replyMessage); return replyMessage; } @@ -1735,7 +1842,7 @@ export class DeviceWrapper { } } - public async sendImage(message: string, community?: boolean) { + public async sendImage(message: string, community?: boolean): Promise { if (this.isIOS()) { // Push file first await this.pushMediaToDevice(testImage); @@ -1785,8 +1892,10 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVideoiOS(message: string) { + public async sendVideoiOS(message: string): Promise { // Push first await this.pushMediaToDevice(testVideo); await this.clickOnElementAll(new AttachmentsButton(this)); @@ -1818,9 +1927,11 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVideoAndroid() { + public async sendVideoAndroid(): Promise { // Push first await this.pushMediaToDevice(testVideo); // Click on attachments button @@ -1874,12 +1985,15 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + this.log(`[DEBUG] sendVideoiOS returning timestamp: ${sentTimestamp}`); + return sentTimestamp; } - public async sendDocument() { + public async sendDocument(): Promise { if (this.isIOS()) { const formattedFileName = 'test_file, pdf'; - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing documents'; copyFileToSimulator(this, testFile); await this.clickOnElementAll(new AttachmentsButton(this)); const keyboard = await this.isKeyboardVisible(); @@ -1963,9 +2077,11 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendGIF() { + public async sendGIF(): Promise { await sleepFor(1000); await this.clickOnByAccessibilityID('Attachments button'); if (this.isAndroid()) { @@ -1988,10 +2104,17 @@ export class DeviceWrapper { if (this.isIOS()) { await this.clickOnElementAll(new SendButton(this)); } + // Checking Sent status on both platforms + await this.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(this).build(), + maxWait: 20000, + }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVoiceMessage() { - await this.longPress('New voice message'); + public async sendVoiceMessage(): Promise { + await this.longPress(new NewVoiceMessageButton(this)); if (this.isAndroid()) { await this.clickOnElementAll({ @@ -2005,12 +2128,19 @@ export class DeviceWrapper { } await this.pressAndHold('New voice message'); + // Checking Sent status on both platforms + await this.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(this).build(), + maxWait: 20000, + }); + const sentTimestamp = Date.now(); + return sentTimestamp; } public async uploadProfilePicture() { await this.clickOnElementAll(new UserSettings(this)); // Click on Profile picture - await this.clickOnElementAll(new UserSettings(this)); + await this.clickOnElementAll(new UserAvatar(this)); await this.clickOnElementAll(new ChangeProfilePictureButton(this)); if (this.isIOS()) { // Push file first @@ -2078,21 +2208,19 @@ export class DeviceWrapper { text: contact.userName, }); } else { - await this.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Contact', - text: contact.userName, - }); + await this.clickOnElementAll(new Contact(this, contact.userName)); } await this.clickOnElementAll(new SendButton(this)); await this.waitForTextElementToBePresent(new OutgoingMessageStatusSent(this)); } public async trustAttachments(conversationName: string) { - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', - }); + // I kept getting stale element references on iOS in this method + // This is an attempt to let the UI settle before we look for the untrusted attachment + if (this.isIOS()) { + await sleepFor(2000); + } + await this.clickOnElementAll({ strategy: 'accessibility id', selector: 'Untrusted attachment message', @@ -2179,18 +2307,16 @@ export class DeviceWrapper { await this.scroll({ x: width / 2, y: height * 0.95 }, { x: width / 2, y: height * 0.35 }, 100); } + public async scrollToBottom() { - try { - const scrollButton = await this.waitForTextElementToBePresent({ - ...new ScrollToBottomButton(this).build(), - maxWait: 1_000, - }); - await this.click(scrollButton.ELEMENT); - } catch { - this.info('Scroll button not found after 1s, continuing'); + if ( + await this.doesElementExist({ ...new ScrollToBottomButton(this).build(), maxWait: 3_000 }) + ) { + await this.clickOnElementAll(new ScrollToBottomButton(this)); + } else { + this.info('Scroll button not found, continuing'); } } - public async pullToRefresh() { if (this.isAndroid()) { await this.pressCoordinates( @@ -2224,32 +2350,8 @@ export class DeviceWrapper { const [primary, fallback] = newAndroid ? [newLocator, legacyLocator] : [legacyLocator, newLocator]; - await this.findWithFallback(primary, fallback); - } - } - - public async closeScreen(newAndroid: boolean = true) { - if (this.isIOS()) { - await this.clickOnByAccessibilityID('Close button'); - return; - } - - if (this.isAndroid()) { - const newLocator = { - strategy: 'id', - selector: 'Close button', - } as StrategyExtractionObj; - - const legacyLocator = { - strategy: 'accessibility id', - selector: 'Navigate up', - } as StrategyExtractionObj; - - const [primary, fallback] = newAndroid - ? [newLocator, legacyLocator] - : [legacyLocator, newLocator]; - - await this.findWithFallback(primary, fallback); + const el = await this.findWithFallback(primary, fallback); + await this.click(el.ELEMENT); } } @@ -2273,7 +2375,7 @@ export class DeviceWrapper { await this.clickOnElementAll(new ReadReceiptsButton(this)); await this.navigateBack(false); await sleepFor(100); - await this.closeScreen(false); + await this.clickOnElementAll(new CloseSettings(this)); } public async processPermissions(locator: LocatorsInterface) { @@ -2370,6 +2472,13 @@ export class DeviceWrapper { return; } + /** + * Checks modal heading and description text against expected values. + * Uses fallback locators to support both new (id) and legacy (accessibility id) variants on Android. + * @param expectedHeading - Expected modal heading string + * @param expectedDescription - Expected modal description string + * @throws Error if heading or description doesn't match expected text + */ public async checkModalStrings(expectedHeading: string, expectedDescription: string) { // Sanitize function removeNewLines(input: string): string { @@ -2377,9 +2486,22 @@ export class DeviceWrapper { return input.replace(/\s*\n+/g, ' ').trim(); } - // Locators - const elHeading = await this.waitForTextElementToBePresent(new ModalHeading(this)); - const elDescription = await this.waitForTextElementToBePresent(new ModalDescription(this)); + // Always try new first, fall back to legacy + const newHeading = new ModalHeading(this).build(); + const legacyHeading = { + strategy: 'accessibility id', + selector: 'Modal heading', + } as StrategyExtractionObj; + + const newDescription = new ModalDescription(this).build(); + const legacyDescription = { + strategy: 'accessibility id', + selector: 'Modal description', + } as StrategyExtractionObj; + + // New → legacy fallback + const elHeading = await this.findWithFallback(newHeading, legacyHeading); + const elDescription = await this.findWithFallback(newDescription, legacyDescription); // Modal Heading const actualHeading = removeNewLines(await this.getTextFromElement(elHeading)); @@ -2412,25 +2534,19 @@ export class DeviceWrapper { } public async getVersionNumber() { - if (this.isIOS()) { - throw new Error('getVersionNumber not implemented on iOS yet'); - } - + // NOTE if this becomes necessary for more tests, consider adding a property/caching to the DeviceWrapper await this.clickOnElementAll(new UserSettings(this)); - await this.scrollDown(); - const versionElement = await this.findElement( - 'id', - 'network.loki.messenger.qa:id/versionTextView' - ); - const versionText = await this.getAttribute('text', versionElement.ELEMENT); + const versionElement = await this.waitForTextElementToBePresent(new VersionNumber(this)); + // Get the full text from the element + const versionText = await this.getTextFromElement(versionElement); + // Extract just the version number (e.g. "1.27.0") const match = versionText?.match(/(\d+\.\d+\.\d+)/); if (!match) { throw new Error(`Could not extract version from: ${versionText}`); } - this.version = match[1]; - return this.version; + return match[1]; } private getUdid() { diff --git a/run/types/allure.ts b/run/types/allure.ts index 1dfd195c9..606b5f40b 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -25,7 +25,7 @@ export type AllureSuiteConfig = | { parent: 'In-App Review Prompt'; suite: 'Flows' | 'Triggers' } | { parent: 'Linkouts' } | { parent: 'New Conversation'; suite: 'Join Community' | 'New Message' } - | { parent: 'Sending Messages'; suite: 'Sending Attachments' } + | { parent: 'Sending Messages'; suite: 'Attachments' | 'Emoji reacts' } | { parent: 'Settings'; suite: 'App Disguise' } | { parent: 'User Actions'; @@ -34,6 +34,7 @@ export type AllureSuiteConfig = | 'Change Profile Picture' | 'Delete Contact' | 'Delete Conversation' + | 'Delete Message' | 'Hide Note to Self' | 'Set Nickname' | 'Share to Session'; @@ -68,21 +69,28 @@ export const TestSteps = { }, // Sending things SEND: { + MESSAGE: (sender: UserNameType, recipient: string) => + `${sender} sends a message to ${recipient}`, LINK: 'Send Link', IMAGE: 'Send Image', + EMOJI_REACT: `Send an emoji react`, }, // Open/Navigate steps OPEN: { + NTS: 'Open Note to Self', UPDATE_GROUP_INFO: `Open 'Update Group Information' modal`, PATH: 'Open Path screen', + APPEARANCE: 'Open Appearance settings', }, // User Actions USER_ACTIONS: { CHANGE_PROFILE_PICTURE: 'Change profile picture', + APP_DISGUISE: 'Set App Disguise', + DELETE_FOR_EVERYONE: 'Delete for everyone', }, // Disappearing Messages DISAPPEARING_MESSAGES: { - SET_DISAPPEARING_MSG: 'Set Disappearing Messages', + SET: (time: string) => `Set Disappearing Messages (${time})`, }, CALLS: { INITIATE_CALL: (userName: UserNameType) => `${userName} initiates voice call`, @@ -90,14 +98,20 @@ export const TestSteps = { }, // Verify steps VERIFY: { + ELEMENT_SCREENSHOT: (elementDesc: string) => + `Verify ${elementDesc} element screenshot matches baseline`, GENERIC_MODAL: 'Verify modal strings', SPECIFIC_MODAL: (modalDesc: string) => `Verify ${modalDesc} modal strings`, + MESSAGE_SYNCED: 'Verify message synced to linked device', MESSAGE_RECEIVED: 'Verify message has been received', MESSAGE_DISAPPEARED: 'Verify message disappeared', + MESSAGE_DELETED: (context: string) => `Verify message deleted in/on ${context}`, + DISAPPEARING_CONTROL_MESSAGES: 'Verify the disappearing control messages for each user', CALLING: 'Verify call has been started', CALL_SUCCESSFUL: 'Verify call has been put through successfully', MISSED_CALL: 'Verify missed call', NICKNAME_CHANGED: (context: string) => `Verify nickname changed in/on ${context}`, PROFILE_PICTURE_CHANGED: 'Verify profile picture has been changed', + EMOJI_REACT: 'Verify emoji react appears for everyone', }, }; diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 6f927cd80..d636a6660 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -1,10 +1,10 @@ // run/types/sessionIt.ts - Clean version matching original pattern import { test, type TestInfo } from '@playwright/test'; -import * as allure from 'allure-js-commons'; import { omit } from 'lodash'; import type { AppCountPerTest } from '../test/specs/state_builder'; +import { setupAllureTestInfo } from '../test/specs/utils/allure/allureHelpers'; import { getNetworkTarget } from '../test/specs/utils/devnet'; import { SupportedPlatformsType } from '../test/specs/utils/open_app'; import { @@ -24,6 +24,11 @@ type MobileItArgs = { shouldSkip?: boolean; allureSuites?: AllureSuiteConfig; allureDescription?: string; + allureLinks?: { + all?: string[] | string; + android?: string[] | string; + ios?: string[] | string; + }; }; export function androidIt(args: Omit) { @@ -43,6 +48,7 @@ function mobileIt({ countOfDevicesNeeded, allureSuites, allureDescription, + allureLinks, }: MobileItArgs) { const testName = `${title} @${platform} @${risk ?? 'default'}-risk @${countOfDevicesNeeded}-devices`; @@ -58,23 +64,27 @@ function mobileIt({ getNetworkTarget(platform); console.info(`\n\n==========> Running "${testName}"\n\n`); - if (allureSuites) { - await allure.parentSuite(allureSuites.parent); - if ('suite' in allureSuites) { - await allure.suite(allureSuites.suite); - } - } - if (allureDescription) { - await allure.description(allureDescription); - } + // Handle Suites, Descriptions and Links + await setupAllureTestInfo({ + suites: allureSuites, + description: allureDescription, + links: allureLinks, + platform, + }); + let testFailed = false; try { await testCb(platform, testInfo); + // If the test passed but used healing, fail loudly to be identified in the allure report const healedAnnotations = testInfo.annotations.filter(a => a.type === 'healed'); if (healedAnnotations.length > 0) { - const details = healedAnnotations.map(a => ` ${a.description}`).join('\n'); + // Deduplicate and sort for consistent error messages + const uniqueHealings = [...new Set(healedAnnotations.map(a => a.description))]; + uniqueHealings.sort(); + + const details = uniqueHealings.join('\n'); throw new Error(`Test passed but used healed locators:\n${details}`); } } catch (error) { diff --git a/run/types/testing.ts b/run/types/testing.ts index 180381ced..68dfa8aad 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -57,7 +57,7 @@ export const InteractionPoints: Record = { BackToSession: { x: 42, y: 42 }, }; -export type Strategy = 'accessibility id' | 'class name' | 'id' | 'xpath'; +export type Strategy = '-android uiautomator' | 'accessibility id' | 'class name' | 'id' | 'xpath'; export type ConversationType = '1:1' | 'Community' | 'Group' | 'Note to Self'; @@ -97,6 +97,11 @@ export type DisappearOptsGroup = [ export type MergedOptions = DisappearOpts1o1 | DisappearOptsGroup; export type StrategyExtractionObj = + | { + strategy: Extract; + selector: UiAutomatorQuery; + text?: string; + } | { strategy: Extract; selector: AccessibilityId; @@ -129,17 +134,24 @@ export type XPath = | `//*[./*[@name='${DISAPPEARING_TIMES}']]/*[2]` | `//*[@resource-id='network.loki.messenger.qa:id/callTitle' and contains(@text, ':')]` | `//*[starts-with(@content-desc, "Photo taken on")]` + | `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]` + | `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${string}"]` + | `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${string}"]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/snippetTextView" and @text="${string}"]` | `//android.widget.TextView[@text="${string}"]` | `//XCUIElementTypeAlert//*//XCUIElementTypeButton` | `//XCUIElementTypeButton[@name="Continue"]` | `//XCUIElementTypeButton[@name="Settings"]` + | `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${string}"]]//XCUIElementTypeStaticText[@value="😂"]` + | `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${string}"]]//XCUIElementTypeStaticText[@value="${string}"]` | `//XCUIElementTypeCell[@name="${string}"]` + | `//XCUIElementTypeCell[@name="Conversation list item" and @label="${string}"]//XCUIElementTypeStaticText[@name="${string}"]` | `//XCUIElementTypeCell[@name="Session"]` | `//XCUIElementTypeImage` | `//XCUIElementTypeOther[contains(@name, "Hey,")][1]` | `//XCUIElementTypeStaticText[@name="Paste"]` | `//XCUIElementTypeStaticText[@name="Videos"]` | `//XCUIElementTypeStaticText[contains(@name, '00:')]` + | `//XCUIElementTypeStaticText[contains(@name, "Version")]` | `//XCUIElementTypeSwitch[@name="Read Receipts, Send read receipts in one-to-one chats."]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[2]/android.widget.Button[1]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.LinearLayout` @@ -148,9 +160,22 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/androidx.appcompat.widget.LinearLayoutCompat/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView[2]` | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; +export type UiAutomatorQuery = + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' + | 'new UiSelector().resourceId("network.loki.messenger.qa:id/messageStatusTextView").text("Sent")' + | 'new UiSelector().text("Enter your display name")' + | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` + | `new UiSelector().text(${string})`; + export type AccessibilityId = | DISAPPEARING_TIMES | UserNameType + | '😂' + | '2' | 'Accept message request' | 'Accept name change' | 'Account ID' @@ -171,20 +196,19 @@ export type AccessibilityId = | 'back' | 'Back' | 'Block' + | 'Block contacts - Navigation' | 'blocked-banner' | 'Blocked banner' - | 'Blocked contacts' - | 'Blocked Contacts' | 'Block message request' | 'Browse' | 'Call' | 'Call button' | 'Cancel' + | 'Classic Light' | 'Clear' | 'Clear all' | 'Close' | 'Close button' - | 'Community input' | 'Community invitation' | 'Configuration message' | 'Confirm' @@ -204,6 +228,7 @@ export type AccessibilityId = | 'Conversations' | 'Copy' | 'Copy button' + | 'Copy URL' | 'Create account button' | 'Create group' | 'Decline message request' @@ -243,6 +268,8 @@ export type AccessibilityId = | 'Empty state label' | 'Enable' | 'End call button' + | 'enjoy-session-negative-button' + | 'enjoy-session-positive-button' | 'Enter Community URL' | 'Enter display name' | 'Error message' @@ -282,6 +309,7 @@ export type AccessibilityId = | 'Manage Members' | 'Media message' | 'MeetingSE' + | 'Meetings option' | 'Mentions list' | 'Message body' | 'Message composition' @@ -306,12 +334,14 @@ export type AccessibilityId = | 'Nickname' | 'No' | 'No pending message requests' + | 'not-now-button' | 'Note to Self' | 'Notifications' | 'Off' | 'OK_BUTTON' | 'OK' | 'Okay' + | 'open-survey-button' | 'Open' | 'Open URL' | 'Path' @@ -320,6 +350,8 @@ export type AccessibilityId = | 'Pin' | 'Please enter a shorter group name' | 'Privacy Policy' + | 'qa-blocked-contacts-settings-item' + | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' | 'Recovery password' @@ -378,6 +410,7 @@ export type AccessibilityId = | 'Voice message' | 'X' | 'Yes' + | 'You have changed the icon for “Session”.' | 'Your message request has been accepted.' | `${DISAPPEARING_TIMES} - Radio` | `${GROUPNAME}` @@ -386,9 +419,11 @@ export type AccessibilityId = export type Id = | DISAPPEARING_TIMES | 'Account ID' + | 'android:id/aerr_close' + | 'android:id/aerr_wait' + | 'android:id/alertTitle' | 'android:id/content_preview_text' | 'android:id/summary' - | 'android:id/text1' | 'android:id/title' | 'android.widget.TextView' | 'Appearance' @@ -398,6 +433,7 @@ export type Id = | 'Call' | 'clear-input-button-description' | 'clear-input-button-name' + | 'clear-input-button' | 'Close button' | 'com.android.chrome:id/negative_button' | 'com.android.chrome:id/signin_fre_dismiss_button' @@ -408,7 +444,7 @@ export type Id = | 'com.android.permissioncontroller:id/permission_deny_button' | 'com.android.settings:id/switch_text' | 'com.google.android.apps.photos:id/sign_in_button' - | 'com.google.android.apps.photos:id/text' + | 'Community input' | 'Confirm invite button' | 'Contact' | 'Contact status' @@ -417,6 +453,7 @@ export type Id = | 'Conversation header name' | 'Conversations' | 'Copy button' + | 'Copy URL' | 'Create account button' | 'Create group' | 'delete-contact-confirm-button' @@ -429,6 +466,7 @@ export type Id = | 'Disable disappearing messages' | 'disappearing-messages-menu-option' | 'Disappearing messages type and time' + | 'Display name' | 'donate-menu-item' | 'Download media' | 'edit-profile-icon' @@ -438,7 +476,6 @@ export type Id = | 'Enter display name' | 'error-message' | 'group-description' - | 'group-name' | 'Group name' | 'Group name input' | 'hide-nts-confirm-button' @@ -474,12 +511,15 @@ export type Id = | 'network.loki.messenger.qa:id/crop_image_menu_crop' | 'network.loki.messenger.qa:id/emptyStateContainer' | 'network.loki.messenger.qa:id/endCallButton' + | 'network.loki.messenger.qa:id/layout_emoji_container' | 'network.loki.messenger.qa:id/linkPreviewView' | 'network.loki.messenger.qa:id/mediapicker_folder_item_thumbnail' | 'network.loki.messenger.qa:id/mediapicker_image_item_thumbnail' | 'network.loki.messenger.qa:id/messageStatusTextView' | 'network.loki.messenger.qa:id/openGroupTitleTextView' | 'network.loki.messenger.qa:id/play_overlay' + | 'network.loki.messenger.qa:id/reaction_1' + | 'network.loki.messenger.qa:id/reactions_pill_count' | 'network.loki.messenger.qa:id/scrollToBottomButton' | 'network.loki.messenger.qa:id/search_cancel' | 'network.loki.messenger.qa:id/search_result_title' @@ -501,6 +541,7 @@ export type Id = | 'preferred-display-name' | 'Privacy' | 'Privacy Policy' + | 'pro-badge-text' | 'Quit' | 'rate-app-button' | 'Recovery password container' @@ -524,6 +565,8 @@ export type Id = | 'update-group-info-confirm-button' | 'update-group-info-description-input' | 'update-group-info-name-input' + | 'update-username-confirm-button' + | 'User settings' | 'Version warning banner' | 'Yes' | `All ${AppName} notifications` @@ -533,8 +576,6 @@ export type TestRisk = 'high' | 'low' | 'medium'; export type ElementStates = 'new_account' | 'restore_account'; -export type PageName = 'network_page' | 'staking_page'; - export type Suffix = 'diff' | 'screenshot'; export type AppName = 'Session AQA' | 'Session QA';