From e2fa12a3deab036646f3b31ec800074d67844ba3 Mon Sep 17 00:00:00 2001 From: wafflesvsfrankie <92288602+burtonemily@users.noreply.github.com> Date: Thu, 29 May 2025 17:42:53 +1000 Subject: [PATCH 01/16] updating tests to match new android test tags --- run/test/specs/community_tests_join.spec.ts | 7 +- .../disappear_after_send_note_to_self.spec.ts | 11 +- run/test/specs/disappearing_call.spec.ts | 20 +-- .../specs/group_message_long_text.spec.ts | 13 +- .../specs/group_tests_add_contact.spec.ts | 7 +- .../specs/group_tests_leave_group.spec.ts | 9 +- .../onboarding_incorrect_seed.spec.ts | 8 +- .../onboarding_long_name.spec.ts | 8 +- .../onboarding_no_name.spec.ts | 8 +- .../onboarding_no_seed.spec.ts | 8 +- .../onboarding_wrong_seed.spec.ts | 8 +- .../specs/linked_device_block_user.spec.ts | 13 +- .../specs/linked_device_create_group.spec.ts | 23 +--- .../linked_device_delete_message.spec.ts | 6 +- .../linked_device_hide_note_to_self.spec.ts | 12 +- .../specs/linked_device_restore_group.spec.ts | 33 ++--- .../linked_device_unsend_message.spec.ts | 12 +- run/test/specs/locators/conversation.ts | 76 ++++++++++- .../specs/locators/disappearing_messages.ts | 72 ++++++++-- run/test/specs/locators/index.ts | 35 +---- run/test/specs/locators/onboarding.ts | 107 ++++++++++----- run/test/specs/locators/settings.ts | 80 ++++++++++- run/test/specs/locators/start_conversation.ts | 125 +++++++++++++----- .../message_community_invitation.spec.ts | 13 +- ...message_requests_accept_text_reply.spec.ts | 9 +- run/test/specs/message_requests_block.spec.ts | 12 +- .../specs/message_requests_decline.spec.ts | 6 +- ...er_actions_block_conversation_list.spec.ts | 4 +- ...actions_block_conversation_options.spec.ts | 4 +- ...ser_actions_hide_recovery_password.spec.ts | 5 +- .../specs/user_actions_set_nickname.spec.ts | 14 +- run/test/specs/utils/create_account.ts | 31 ++--- run/test/specs/utils/create_group.ts | 27 ++-- run/test/specs/utils/join_community.ts | 3 +- run/test/specs/utils/link_device.ts | 25 ++-- run/test/specs/utils/restore_account.ts | 39 +++--- .../specs/utils/set_disappearing_messages.ts | 14 +- run/test/specs/voice_calls.spec.ts | 63 ++++++--- .../specs/warning_modal_new_account.spec.ts | 2 +- .../warning_modal_restore_account.spec.ts | 2 +- run/types/DeviceWrapper.ts | 40 ++++-- run/types/testing.ts | 39 +++++- 42 files changed, 640 insertions(+), 413 deletions(-) diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index 230ad2f4..35472b19 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -1,5 +1,6 @@ import { testCommunityLink, testCommunityName } from '../../constants/community'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { ConversationItem } from './locators/home'; import { open_Alice2 } from './state_builder'; import { joinCommunity } from './utils/join_community'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -21,10 +22,6 @@ async function joinCommunityTest(platform: SupportedPlatformsType) { await alice1.onIOS().scrollToBottom(); await alice1.sendMessage(testMessage); // Has community synced to device 2? - await alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testCommunityName, - }); + await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); await closeApp(alice1, alice2); } 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 f6b0dde0..6610873f 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,6 +1,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; -import { EnterAccountID } from './locators/start_conversation'; +import { PlusButton } from './locators/home'; +import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -20,11 +21,11 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType) { const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; // Send message to self to bring up Note to Self conversation - await device.clickOnByAccessibilityID('New conversation button'); - await device.clickOnByAccessibilityID('New direct message'); + 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.clickOnByAccessibilityID('Next'); + await device.clickOnElementAll(new NextButton(device)); await device.inputText('Creating note to self', { strategy: 'accessibility id', selector: 'Message input box', @@ -53,5 +54,3 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType) { // Great success await closeApp(device); } - -// TO DO - ADD TEST TO TURN OFF DISAPPEARING MESSAGES diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 6fd4886c..86620fc2 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -1,5 +1,7 @@ import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { CallButton } from './locators/conversation'; +import { ContinueButton } from './locators/global'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -31,8 +33,7 @@ async function disappearingCallMessage1o1Ios(platform: SupportedPlatformsType) { focusFriendsConvo: true, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - // await alice1.navigateBack(); - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Enabled voice calls in privacy settings await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', @@ -46,7 +47,7 @@ async function disappearingCallMessage1o1Ios(platform: SupportedPlatformsType) { strategy: 'accessibility id', selector: 'Allow voice and video calls', }); - await alice1.clickOnByAccessibilityID('Continue'); + await alice1.clickOnElementAll(new ContinueButton(alice1)); // Navigate back to conversation await sleepFor(500); await alice1.clickOnByAccessibilityID('Close button'); @@ -62,7 +63,7 @@ async function disappearingCallMessage1o1Ios(platform: SupportedPlatformsType) { await sleepFor(500, true); await bob1.clickOnByAccessibilityID('Close button'); // Make call on device 1 (alice) - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Answer call on device 2 await bob1.clickOnByAccessibilityID('Answer call'); // Wait 30 seconds @@ -98,15 +99,12 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp focusFriendsConvo: true, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - - // await alice1.navigateBack(); - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Enabled voice calls in privacy settings await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Settings', }); - // Scroll to bottom of page to voice and video calls await sleepFor(1000); await alice1.scrollDown(); @@ -115,7 +113,6 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp selector: 'android:id/summary', text: 'Enables voice and video calls to and from other users.', }); - await alice1.click(voicePermissions.ELEMENT); // Toggle voice settings on // Click enable on exposure IP address warning @@ -128,7 +125,6 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - await alice1.navigateBack(); // Enable voice calls on device 2 for User B await bob1.clickOnByAccessibilityID('Call'); @@ -138,7 +134,6 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp selector: 'Settings', text: 'Settings', }); - await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Settings', @@ -151,7 +146,6 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp selector: 'android:id/summary', text: 'Enables voice and video calls to and from other users.', }); - await bob1.click(voicePermissions2.ELEMENT); // Toggle voice settings on // Click enable on exposure IP address warning @@ -166,7 +160,7 @@ async function disappearingCallMessage1o1Android(platform: SupportedPlatformsTyp ); await bob1.navigateBack(); // Make call on device 1 (alice) - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Answer call on device 2 await bob1.clickOnByAccessibilityID('Answer call'); // Wait 5 seconds diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index 567032da..3cce2d27 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -3,6 +3,7 @@ import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; import { OutgoingMessageStatusSent } from './locators/conversation'; +import { ConversationItem } from './locators/home'; bothPlatformsItSeparate({ title: 'Send long message to group', @@ -95,11 +96,7 @@ async function sendLongMessageGroupAndroid(platform: SupportedPlatformsType) { 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({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, - }); + await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Message body', @@ -107,11 +104,7 @@ async function sendLongMessageGroupAndroid(platform: SupportedPlatformsType) { }); // Go out and back into the group to see the last message await charlie1.navigateBack(); - await charlie1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, - }); + await charlie1.clickOnElementAll(new ConversationItem(charlie1, testGroupName)); await charlie1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Message body', diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index feba3a03..c26879dd 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -5,6 +5,7 @@ import { EditGroup, InviteContactsButton, InviteContactsMenuItem } from './locat import { ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; import { InviteContactConfirm } from './locators/groups'; +import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; @@ -33,11 +34,7 @@ async function addContactToGroup(platform: SupportedPlatformsType) { // Exit to conversation list await alice1.navigateBack(); // Select group conversation in list - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: group.groupName, - }); + await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); // Click more options await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Select edit group diff --git a/run/test/specs/group_tests_leave_group.spec.ts b/run/test/specs/group_tests_leave_group.spec.ts index 11979bdd..225538e8 100644 --- a/run/test/specs/group_tests_leave_group.spec.ts +++ b/run/test/specs/group_tests_leave_group.spec.ts @@ -1,7 +1,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationSettings } from './locators/conversation'; -import { LeaveGroupButton } from './locators/groups'; +import { LeaveGroupButton, LeaveGroupConfirm } from './locators/groups'; +import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils/index'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -28,7 +29,7 @@ async function leaveGroup(platform: SupportedPlatformsType) { await sleepFor(1000); await charlie1.clickOnElementAll(new LeaveGroupButton(charlie1)); // Modal with Leave/Cancel - await charlie1.clickOnByAccessibilityID('Leave'); + await charlie1.clickOnElementAll(new LeaveGroupConfirm(charlie1)); // Check for control message const groupMemberLeft = englishStrippedStr('groupMemberLeft') .withArgs({ name: charlie.userName }) @@ -37,9 +38,7 @@ async function leaveGroup(platform: SupportedPlatformsType) { await bob1.waitForControlMessageToBePresent(groupMemberLeft); // Check device 3 that group has disappeared await charlie1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, + ...new ConversationItem(charlie1, testGroupName).build(), maxWait: 5000, }); await closeApp(alice1, bob1, charlie1); diff --git a/run/test/specs/input_validations/onboarding_incorrect_seed.spec.ts b/run/test/specs/input_validations/onboarding_incorrect_seed.spec.ts index 68be409b..bc2269b2 100644 --- a/run/test/specs/input_validations/onboarding_incorrect_seed.spec.ts +++ b/run/test/specs/input_validations/onboarding_incorrect_seed.spec.ts @@ -1,12 +1,8 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../../types/sessionIt'; -import { - AccountRestoreButton, - ContinueButton, - ErrorMessage, - SeedPhraseInput, -} from '../locators/onboarding'; +import { AccountRestoreButton, ErrorMessage, SeedPhraseInput } from '../locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { ContinueButton } from '../locators/global'; bothPlatformsIt({ title: 'Onboarding incorrect seed', diff --git a/run/test/specs/input_validations/onboarding_long_name.spec.ts b/run/test/specs/input_validations/onboarding_long_name.spec.ts index c6b3d98c..2976e358 100644 --- a/run/test/specs/input_validations/onboarding_long_name.spec.ts +++ b/run/test/specs/input_validations/onboarding_long_name.spec.ts @@ -1,12 +1,8 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../../types/sessionIt'; -import { - ContinueButton, - CreateAccountButton, - DisplayNameInput, - ErrorMessage, -} from '../locators/onboarding'; +import { CreateAccountButton, DisplayNameInput, ErrorMessage } from '../locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { ContinueButton } from '../locators/global'; bothPlatformsIt({ title: 'Onboarding long name', diff --git a/run/test/specs/input_validations/onboarding_no_name.spec.ts b/run/test/specs/input_validations/onboarding_no_name.spec.ts index 8b5acd6e..99829314 100644 --- a/run/test/specs/input_validations/onboarding_no_name.spec.ts +++ b/run/test/specs/input_validations/onboarding_no_name.spec.ts @@ -1,12 +1,8 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../../types/sessionIt'; -import { - ContinueButton, - CreateAccountButton, - DisplayNameInput, - ErrorMessage, -} from '../locators/onboarding'; +import { CreateAccountButton, DisplayNameInput, ErrorMessage } from '../locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { ContinueButton } from '../locators/global'; bothPlatformsIt({ title: 'Onboarding no name', diff --git a/run/test/specs/input_validations/onboarding_no_seed.spec.ts b/run/test/specs/input_validations/onboarding_no_seed.spec.ts index 7028c84f..f2a6c145 100644 --- a/run/test/specs/input_validations/onboarding_no_seed.spec.ts +++ b/run/test/specs/input_validations/onboarding_no_seed.spec.ts @@ -1,12 +1,8 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../../types/sessionIt'; -import { - AccountRestoreButton, - ContinueButton, - ErrorMessage, - SeedPhraseInput, -} from '../locators/onboarding'; +import { AccountRestoreButton, ErrorMessage, SeedPhraseInput } from '../locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { ContinueButton } from '../locators/global'; bothPlatformsIt({ title: 'Onboarding no seed', diff --git a/run/test/specs/input_validations/onboarding_wrong_seed.spec.ts b/run/test/specs/input_validations/onboarding_wrong_seed.spec.ts index eaad9f22..d883634a 100644 --- a/run/test/specs/input_validations/onboarding_wrong_seed.spec.ts +++ b/run/test/specs/input_validations/onboarding_wrong_seed.spec.ts @@ -1,12 +1,8 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../../types/sessionIt'; -import { - AccountRestoreButton, - ContinueButton, - ErrorMessage, - SeedPhraseInput, -} from '../locators/onboarding'; +import { AccountRestoreButton, ErrorMessage, SeedPhraseInput } from '../locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { ContinueButton } from '../locators/global'; bothPlatformsIt({ title: 'Onboarding wrong seed', diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 2c253617..075e6fc1 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -2,7 +2,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal } from './locators'; import { ConversationSettings } from './locators/conversation'; -import { UserSettings } from './locators/settings'; +import { ConversationItem } from './locators/home'; +import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice2_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -44,11 +45,7 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) }); if (blockedStatus) { // Check linked device for blocked status (if shown on alice1) - await alice2.onAndroid().clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: `${bob.userName}`, - }); + await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName)) await alice2.onAndroid().waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Blocked banner', @@ -64,8 +61,8 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) alice2.clickOnElementAll(new UserSettings(alice2)), ]); await Promise.all([ - alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }), - alice2.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }), + alice1.clickOnElementAll(new ConversationsMenuItem(alice1)), + alice2.clickOnElementAll(new ConversationsMenuItem(alice2)), ]); await Promise.all([ alice1.clickOnElementAll(new BlockedContactsSettings(alice1)), diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 93d624cf..6fc851e9 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -2,8 +2,9 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { EditGroup, EditGroupName } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationHeaderName, ConversationSettings } from './locators/conversation'; import { EditGroupNameInput } from './locators/groups'; +import { ConversationItem } from './locators/home'; import { SaveNameChangeButton } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; @@ -35,11 +36,7 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { // Note we keep this createGroup here as we want it to **indeed** use the UI to create the group await createGroup(platform, device1, alice, device3, bob, device4, charlie, testGroupName); // Test that group has loaded on linked device - await device2.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, - }); + await device2.clickOnElementAll(new ConversationItem(device2, testGroupName)); // Change group name in device 1 // Click on settings/more info await device1.clickOnElementAll(new ConversationSettings(device1)); @@ -69,11 +66,9 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { // Wait 5 seconds for name to update await sleepFor(5000); // Check linked device for name change (conversation header name) - await device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation header name', - text: newGroupName, - }); + await device2.waitForTextElementToBePresent( + new ConversationHeaderName(device2).build(newGroupName) + ); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), @@ -96,11 +91,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType) { // Note we keep this createGroup here as we want it to **indeed** use the UI to create the group await createGroup(platform, device1, alice, device3, bob, device4, charlie, testGroupName); // Test that group has loaded on linked device - await device2.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, - }); + await device2.clickOnElementAll(new ConversationItem(device2, testGroupName)); // Click on settings or three dots await device1.clickOnElementAll(new ConversationSettings(device1)); // Click on Edit group option diff --git a/run/test/specs/linked_device_delete_message.spec.ts b/run/test/specs/linked_device_delete_message.spec.ts index 94c50ddb..557f63d3 100644 --- a/run/test/specs/linked_device_delete_message.spec.ts +++ b/run/test/specs/linked_device_delete_message.spec.ts @@ -2,6 +2,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal } from './locators'; import { DeletedMessage } from './locators/conversation'; +import { ConversationItem } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -22,10 +23,7 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType) { const sentMessage = await alice1.sendMessage(testMessage); // Check message came through on linked device(3) // Enter conversation with user B on device 3 - await alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - }); + await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, bob.userName)); await alice2.selectByText('Conversation list item', bob.userName); // Find message await alice2.findMessageWithBody(sentMessage); diff --git a/run/test/specs/linked_device_hide_note_to_self.spec.ts b/run/test/specs/linked_device_hide_note_to_self.spec.ts index b8035bde..6da2cc11 100644 --- a/run/test/specs/linked_device_hide_note_to_self.spec.ts +++ b/run/test/specs/linked_device_hide_note_to_self.spec.ts @@ -2,7 +2,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { EmptyConversation, Hide } from './locators/conversation'; import { CancelSearchButton, NoteToSelfOption } from './locators/global_search'; -import { SearchButton } from './locators/home'; +import { ConversationItem, SearchButton } from './locators/home'; import { open_Alice2 } from './state_builder'; import { SupportedPlatformsType } from './utils/open_app'; @@ -34,11 +34,7 @@ async function hideNoteToSelf(platform: SupportedPlatformsType) { await alice1.sendMessage('Creating note to self'); await alice1.navigateBack(); // Does note to self appear on linked device - await alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: noteToSelf, - }); + await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, noteToSelf)); await alice1.clickOnElementAll(new CancelSearchButton(alice1)); await alice1.onIOS().swipeLeft('Conversation list item', noteToSelf); await alice1.onAndroid().longPressConversation(noteToSelf); @@ -51,9 +47,7 @@ async function hideNoteToSelf(platform: SupportedPlatformsType) { await Promise.all( [alice1, alice2].map(device => device.doesElementExist({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: noteToSelf, + ...new ConversationItem(alice2, noteToSelf).build(), maxWait: 5000, }) ) diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 98482963..1e43d045 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -1,5 +1,6 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; import { closeApp, openAppFourDevices, SupportedPlatformsType } from './utils/open_app'; @@ -26,11 +27,7 @@ async function restoreGroup(platform: SupportedPlatformsType) { const charlieMessage = `${USERNAME.CHARLIE} to ${testGroupName}`; await restoreAccount(device4, alice); // Check that group has loaded on linked device - await device4.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testGroupName, - }); + await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded await device4.waitForTextElementToBePresent({ strategy: 'accessibility id', @@ -57,22 +54,14 @@ async function restoreGroup(platform: SupportedPlatformsType) { ]); const testMessage2 = 'Checking that message input is working'; await device4.sendMessage(testMessage2); - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage2, - }), - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage2, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage2, - }), - ]); + await Promise.all( + [device1, device2, device3].map(device => + device.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'Message body', + text: 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 037bd226..9e6d281e 100644 --- a/run/test/specs/linked_device_unsend_message.spec.ts +++ b/run/test/specs/linked_device_unsend_message.spec.ts @@ -2,6 +2,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; import { DeletedMessage } from './locators/conversation'; +import { ConversationItem } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -22,15 +23,8 @@ async function unSendMessageLinkedDevice(platform: SupportedPlatformsType) { 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({ - strategy: 'accessibility id', - selector: 'Conversation list item', - }); - await alice2.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: bob.userName, - }); + 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 diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 0ef85161..28eba000 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -11,10 +11,18 @@ export class MessageInput extends LocatorsInterface { export class ConversationSettings extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'More options', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'conversation-options-avatar', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'More options', + } as const; + } } } @@ -93,3 +101,63 @@ export class OutgoingMessageStatusSent extends LocatorsInterface { } as const; } } + +export class CallButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'accessibility id', + selector: 'Call button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Call', + } as const; + } + } +} + +export class ConversationHeaderName extends LocatorsInterface { + public build(text?: string) { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Conversation header name', + text, + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Conversation header name', + text, + } as const; + } + } +} + +export class NotificationSettings extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'Notifications', + } as const; + } +} + +export class NotificationSwitch extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'com.android.settings:id/switch_text', + text: 'All Session notifications', + } as const; + case 'ios': + throw new Error('Platform not supported'); + } + } +} diff --git a/run/test/specs/locators/disappearing_messages.ts b/run/test/specs/locators/disappearing_messages.ts index 9c275048..48d0377e 100644 --- a/run/test/specs/locators/disappearing_messages.ts +++ b/run/test/specs/locators/disappearing_messages.ts @@ -1,7 +1,10 @@ import { LocatorsInterface } from '.'; -import { StrategyExtractionObj } from '../../../types/testing'; -import { DISAPPEARING_TIMES } from '../../../types/testing'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; +import { + DISAPPEARING_TIMES, + DisappearingOptions, + StrategyExtractionObj, +} from '../../../types/testing'; export class DisappearingMessagesMenuOption extends LocatorsInterface { public build(): StrategyExtractionObj { @@ -9,8 +12,7 @@ export class DisappearingMessagesMenuOption extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: `network.loki.messenger:id/title`, - text: 'Disappearing messages', + selector: 'disappearing-messages-menu-option', }; case 'ios': return { @@ -25,6 +27,10 @@ export class DisappearingMessagesSubtitle extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'Disappearing messages type and time', + }; case 'ios': return { strategy: 'accessibility id', @@ -39,7 +45,7 @@ export class DisableDisappearingMessages extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: `Disable disappearing messages`, }; case 'ios': @@ -52,10 +58,18 @@ export class DisableDisappearingMessages extends LocatorsInterface { } export class SetDisappearMessagesButton extends LocatorsInterface { public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Set button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Set button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Set button', + } as const; + } } } @@ -93,9 +107,41 @@ export class DisappearingMessageRadial extends LocatorsInterface { this.timer = timer; } public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: `${this.timer} - Radio`, - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: this.timer, + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: `${this.timer} - Radio`, + } as const; + } + } +} + +export class DisappearingMessagesTimerType extends LocatorsInterface { + private timerType: DisappearingOptions; + + constructor(device: DeviceWrapper, timerType: DisappearingOptions) { + super(device); + this.timerType = timerType; + } + + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: this.timerType, + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: this.timerType, + } as const; + } } } diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index a1915c73..aed49394 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -96,21 +96,6 @@ export class EditGroupName extends LocatorsInterface { } } -export class PrivacyButton extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'class name', - selector: 'android.widget.TextView', - text: 'Privacy', - } as const; - case 'ios': - return { strategy: 'id', selector: 'Privacy' } as const; - } - } -} - export class ReadReceiptsButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -224,8 +209,7 @@ export class BlockUser extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/title', - text: 'Block', + selector: 'block-user-confirm-button', }; } } @@ -470,23 +454,6 @@ export class DeleteMesssageRequestConfirmation extends LocatorsInterface { } } -export class RevealRecoveryPhraseButton extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'accessibility id', - selector: 'Reveal recovery phrase button', - }; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Continue', - }; - } - } -} - export class DownloadMediaButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/specs/locators/onboarding.ts b/run/test/specs/locators/onboarding.ts index 95acfad3..a9ad3e28 100644 --- a/run/test/specs/locators/onboarding.ts +++ b/run/test/specs/locators/onboarding.ts @@ -3,21 +3,20 @@ import { LocatorsInterface } from './index'; // SHARED LOCATORS -export class ContinueButton extends LocatorsInterface { - public build() { - return { - strategy: 'accessibility id', - selector: 'Continue', - } as const; - } -} - export class ErrorMessage extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Error message', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'error-message', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Error message', + } as const; + } } } @@ -50,31 +49,54 @@ export class WarningModalQuitButton extends LocatorsInterface { // SPLASH SCREEN export class CreateAccountButton extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Create account button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Create account button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Create account button', + } as const; + } } } export class AccountRestoreButton extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Restore your session button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Restore your session button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Restore your session button', + } as const; + } } } export class SplashScreenLinks extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Open URL', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Open URL', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Open URL', + } as const; + } } } - export class TermsOfServiceButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -151,9 +173,34 @@ export class SeedPhraseInput extends LocatorsInterface { export class SlowModeRadio extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Slow mode notifications button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Slow mode notifications button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Slow mode notifications button', + } as const; + } + } +} + +export class LoadingAnimation extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Loading animation', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Loading animation', + } as const; + } } } diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 2a80fb96..f0a9c949 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -3,10 +3,18 @@ import { LocatorsInterface } from './index'; export class HideRecoveryPasswordButton extends LocatorsInterface { public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Hide recovery password button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Hide recovery password button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Hide recovery password button', + } as const; + } } } @@ -53,6 +61,40 @@ export class RecoveryPasswordMenuItem extends LocatorsInterface { } } +export class RevealRecoveryPhraseButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Reveal recovery phrase button', + }; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Continue', + }; + } + } +} + +export class RecoveryPhraseContainer extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Recovery password container', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Recovery password container', + } as const; + } + } +} + export class SaveProfilePictureButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -104,3 +146,33 @@ export class BlockedContacts extends LocatorsInterface { } } } +export class PrivacyMenuItem extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Privacy', + } as const; + case 'ios': + return { strategy: 'id', selector: 'Privacy' } as const; + } + } +} + +export class ConversationsMenuItem extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Conversations', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Conversations', + } as const; + } + } +} diff --git a/run/test/specs/locators/start_conversation.ts b/run/test/specs/locators/start_conversation.ts index c9a5feb1..2c7b911c 100644 --- a/run/test/specs/locators/start_conversation.ts +++ b/run/test/specs/locators/start_conversation.ts @@ -3,19 +3,35 @@ import { LocatorsInterface } from './index'; export class NewMessageOption extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'New direct message', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'New direct message', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'New direct message', + } as const; + } } } export class CreateGroupOption extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Create group', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Create group', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Create group', + } as const; + } } } @@ -24,7 +40,7 @@ export class JoinCommunityOption extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Join community button', }; case 'ios': @@ -38,10 +54,18 @@ export class JoinCommunityOption extends LocatorsInterface { export class InviteAFriendOption extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Invite friend button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Invite friend button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Invite friend button', + } as const; + } } } @@ -61,6 +85,40 @@ export class CloseButton extends LocatorsInterface { } } } + +export class CopyButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Copy button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Copy button', + } as const; + } + } +} + +export class NextButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Next', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Next', + } as const; + } + } +} // NEW MESSAGE SECTION export class EnterAccountID extends LocatorsInterface { public build(): StrategyExtractionObj { @@ -82,27 +140,34 @@ export class EnterAccountID extends LocatorsInterface { // INVITE A FRIEND SECTION export class AccountIDField extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Account ID', - } as const; + 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() { - return { - strategy: 'accessibility id', - selector: 'Share button', - } as const; - } -} - -export class CopyButton extends LocatorsInterface { - public build() { - return { - strategy: 'accessibility id', - selector: 'Copy button', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Share button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Share button', + } as const; + } } } diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index fd86b9f1..b0c18e1d 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -7,6 +7,7 @@ import { testCommunityLink, testCommunityName } from './../../constants/communit import { ConversationSettings } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { ConversationItem } from './locators/home'; bothPlatformsItSeparate({ title: 'Send community invitation', @@ -62,11 +63,7 @@ async function sendCommunityInvitationIos(platform: SupportedPlatformsType) { ); await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Join' }); await bob1.navigateBack(); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testCommunityName, - }); + await bob1.waitForTextElementToBePresent(new ConversationItem(bob1, testCommunityName)); await closeApp(alice1, bob1); } @@ -117,10 +114,6 @@ async function sendCommunityInviteMessageAndroid(platform: SupportedPlatformsTyp ); await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Join' }); await bob1.navigateBack(); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: testCommunityName, - }); + await bob1.waitForTextElementToBePresent(new ConversationItem(bob1, testCommunityName)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_requests_accept_text_reply.spec.ts b/run/test/specs/message_requests_accept_text_reply.spec.ts index c76fb595..3aa32dd7 100644 --- a/run/test/specs/message_requests_accept_text_reply.spec.ts +++ b/run/test/specs/message_requests_accept_text_reply.spec.ts @@ -1,7 +1,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { EnterAccountID } from './locators/start_conversation'; +import { PlusButton } from './locators/home'; +import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; import { SupportedPlatformsType, closeApp, openAppTwoDevices } from './utils/open_app'; @@ -22,14 +23,14 @@ async function acceptRequestWithText(platform: SupportedPlatformsType) { ]); const testMessage = `${alice.userName} to ${bob.userName}`; // Send message from Alice to Bob - await device1.clickOnByAccessibilityID('New conversation button'); + await device1.clickOnElementAll(new PlusButton(device1)); // Select direct message option - await device1.clickOnByAccessibilityID('New direct message'); + await device1.clickOnElementAll(new NewMessageOption(device1)); // Enter User B's session ID into input box await device1.inputText(bob.accountID, new EnterAccountID(device1)); // Click next await device1.scrollDown(); - await device1.clickOnByAccessibilityID('Next'); + await device1.clickOnElementAll(new NextButton(device1)); //messageRequestPendingDescription: "You will be able to send voice messages and attachments once the recipient has approved this message request." const messageRequestPendingDescription = englishStrippedStr( 'messageRequestPendingDescription' diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index eb514810..11c476d8 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -2,7 +2,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME, type AccessibilityId } from '../../types/testing'; import { BlockedContactsSettings, BlockUserConfirmationModal } from './locators'; -import { UserSettings } from './locators/settings'; +import { PlusButton } from './locators/home'; +import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; @@ -63,10 +64,7 @@ async function blockedRequest(platform: SupportedPlatformsType) { const blockedMessage = `"${alice.userName} to ${bob.userName} - shouldn't get through"`; await device1.sendMessage(blockedMessage); await device2.navigateBack(); - await device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'New conversation button', - }); + await device2.waitForTextElementToBePresent(new PlusButton(device2)); // Need to wait to see if message gets through await sleepFor(5000); await device2.hasTextElementBeenDeleted('Message body', blockedMessage); @@ -77,8 +75,8 @@ async function blockedRequest(platform: SupportedPlatformsType) { device3.clickOnElementAll(new UserSettings(device3)), ]); await Promise.all([ - device2.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }), - device3.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }), + device2.clickOnElementAll(new ConversationsMenuItem(device2)), + device3.clickOnElementAll(new ConversationsMenuItem(device3)), ]); await Promise.all([ device2.clickOnElementAll(new BlockedContactsSettings(device2)), diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index de4cf1e0..1e1eabcd 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -2,6 +2,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME, type AccessibilityId } from '../../types/testing'; import { DeclineMessageRequestButton, DeleteMesssageRequestConfirmation } from './locators'; +import { PlusButton } from './locators/home'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; @@ -59,10 +60,7 @@ async function declineRequest(platform: SupportedPlatformsType) { await sleepFor(100); await device2.navigateBack(); // Look for new conversation button to make sure it all worked - await device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'New conversation button', - }); + await device2.waitForTextElementToBePresent(new PlusButton(device2)); // Close app await closeApp(device1, device2, device3); } 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 50d9aecf..86d37a48 100644 --- a/run/test/specs/user_actions_block_conversation_list.spec.ts +++ b/run/test/specs/user_actions_block_conversation_list.spec.ts @@ -3,7 +3,7 @@ import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BlockedContactsSettings, BlockUserConfirmationModal } from './locators'; import { LongPressBlockOption } from './locators/home'; -import { UserSettings } from './locators/settings'; +import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -44,7 +44,7 @@ async function blockUserInConversationList(platform: SupportedPlatformsType) { }); await alice1.navigateBack(); await alice1.clickOnElementAll(new UserSettings(alice1)); - await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }); + await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', 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 8b2d5fc6..4d24a543 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -7,7 +7,7 @@ import { ExitUserProfile, } from './locators'; import { ConversationSettings } from './locators/conversation'; -import { UserSettings } from './locators/settings'; +import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -58,7 +58,7 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) // Check settings for blocked user await alice1.navigateBack(); await alice1.clickOnElementAll(new UserSettings(alice1)); - await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Conversations' }); + await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); // Accessibility ID for Blocked Contact not present on iOS await alice1.waitForTextElementToBePresent({ 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 389ff868..fc5731c1 100644 --- a/run/test/specs/user_actions_hide_recovery_password.spec.ts +++ b/run/test/specs/user_actions_hide_recovery_password.spec.ts @@ -23,10 +23,7 @@ async function hideRecoveryPassword(platform: SupportedPlatformsType) { await linkedDevice(device1, device2, USERNAME.ALICE); await device1.clickOnElementAll(new UserSettings(device1)); await device1.scrollDown(); - await device1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Recovery password menu item', - }); + await device1.clickOnElementAll(new RecoveryPasswordMenuItem(device1)); await device1.clickOnElementAll(new HideRecoveryPasswordButton(device1)); // Wait for modal to appear // Check modal is correct diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index ef6e2931..a0d899a4 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -2,7 +2,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { UsernameInput } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationHeaderName, ConversationSettings } from './locators/conversation'; import { SaveNameChangeButton } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -71,7 +71,7 @@ async function setNicknameAndroid(platform: SupportedPlatformsType) { }); const nickName = 'New nickname'; // Go back to conversation list - await alice1.navigateBack(); + await alice1.navigateBack(true); // Select conversation in list with Bob await alice1.longPressConversation(bob.userName); // Select 'Details' option @@ -85,13 +85,13 @@ async function setNicknameAndroid(platform: SupportedPlatformsType) { // CLick out of pop up await alice1.clickOnByAccessibilityID('Message user'); // Check name at top of conversation is nickname - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation header name', - }); + await alice1.waitForTextElementToBePresent(new ConversationHeaderName(alice1)); // Send a message so nickname is updated in conversation list await alice1.sendMessage('Message to test nickname change'); - const actualNickname = await alice1.grabTextFromAccessibilityId('Conversation header name'); + const conversationHeader = await alice1.waitForTextElementToBePresent( + new ConversationHeaderName(alice1) + ); + const actualNickname = await alice1.getTextFromElement(conversationHeader); if (actualNickname !== nickName) { throw new Error('Nickname has not been changed in header'); } diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 1681cc10..8af0f090 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -2,26 +2,24 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; -import { RevealRecoveryPhraseButton } from '../locators'; -import { DisplayNameInput } from '../locators/onboarding'; +import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; +import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locators/onboarding'; import { UserSettings } from '../locators/settings'; +import { ContinueButton } from '../locators/global'; +import { CopyButton } from '../locators/start_conversation'; export const newUser = async (device: DeviceWrapper, userName: UserNameType): Promise => { // Click create session ID - - await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Create account button', - }); + await device.clickOnElementAll(new CreateAccountButton(device)); // Input username await device.inputText(userName, new DisplayNameInput(device)); // Click continue - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // Choose message notification options // Want to choose 'Slow Mode' so notifications don't interrupt test - await device.clickOnByAccessibilityID('Slow mode notifications button'); + await device.clickOnElementAll(new SlowModeRadio(device)); // Select Continue to save notification settings - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // TODO need to retry check every 1s for 5s console.warn('about to look for Allow permission in 5s'); await sleepFor(5000); @@ -30,16 +28,15 @@ export const newUser = async (device: DeviceWrapper, userName: UserNameType): Pr console.warn('looked for Allow permission'); await sleepFor(1000); // Click on 'continue' button to open recovery phrase modal - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Reveal recovery phrase button', - }); + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); //Save recovery password - await device.clickOnByAccessibilityID('Recovery password container'); - await device.onAndroid().clickOnByAccessibilityID('Copy button'); + const recoveryPhraseContainer = await device.clickOnElementAll( + new RecoveryPhraseContainer(device) + ); + await device.onAndroid().clickOnElementAll(new CopyButton(device)); // Save recovery phrase as variable - const recoveryPhrase = await device.grabTextFromAccessibilityId('Recovery password container'); + const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); // Exit Modal await device.navigateBack(); diff --git a/run/test/specs/utils/create_group.ts b/run/test/specs/utils/create_group.ts index b32bbbd5..9a2c1342 100644 --- a/run/test/specs/utils/create_group.ts +++ b/run/test/specs/utils/create_group.ts @@ -3,6 +3,8 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { Group, GROUPNAME, User } from '../../../types/testing'; import { Contact } from '../locators/global'; import { CreateGroupButton, GroupNameInput } from '../locators/groups'; +import { ConversationItem, PlusButton } from '../locators/home'; +import { CreateGroupOption } from '../locators/start_conversation'; import { newContact } from './create_contact'; import { sortByPubkey } from './get_account_id'; import { SupportedPlatformsType } from './open_app'; @@ -25,18 +27,18 @@ export const createGroup = async ( const charlieMessage = `${userThree.userName} to ${userName}`; // Create contact between User A and User B await newContact(platform, device1, userOne, device2, userTwo); - await device1.navigateBack(); + await device1.navigateBack(true); await newContact(platform, device1, userOne, device3, userThree); - await device2.navigateBack(); + await device2.navigateBack(true); // Create contact between User A and User C // Exit conversation back to list - await device1.navigateBack(); + await device1.navigateBack(true); // Exit conversation back to list - await device3.navigateBack(); + await device3.navigateBack(true); // Click plus button - await device1.clickOnByAccessibilityID('New conversation button'); + await device1.clickOnElementAll(new PlusButton(device1)); // Select Closed Group option - await device1.clickOnByAccessibilityID('Create group'); + await device1.clickOnElementAll(new CreateGroupOption(device1)); // Type in group name await device1.inputText(userName, new GroupNameInput(device1)); // Select User B and User C @@ -45,17 +47,10 @@ export const createGroup = async ( // Select tick await device1.clickOnElementAll(new CreateGroupButton(device1)); // Enter group chat on device 2 and 3 + await Promise.all([device2.onAndroid().navigateBack(), device3.onAndroid().navigateBack()]); await Promise.all([ - device2.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: group.userName, - }), - device3.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: group.userName, - }), + device2.clickOnElementAll(new ConversationItem(device2, group.userName)), + device3.clickOnElementAll(new ConversationItem(device3, group.userName)), ]); if (checkControlMessage) { // Sort by account ID diff --git a/run/test/specs/utils/join_community.ts b/run/test/specs/utils/join_community.ts index c2f124fb..55b861ae 100644 --- a/run/test/specs/utils/join_community.ts +++ b/run/test/specs/utils/join_community.ts @@ -1,5 +1,6 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { CommunityInput, JoinCommunityButton } from '../locators'; +import { PlusButton } from '../locators/home'; import { JoinCommunityOption } from '../locators/start_conversation'; export const joinCommunity = async ( @@ -7,7 +8,7 @@ export const joinCommunity = async ( communityLink: string, communityName: string ) => { - await device.clickOnByAccessibilityID('New conversation button'); + await device.clickOnElementAll(new PlusButton(device)); await device.clickOnElementAll(new JoinCommunityOption(device)); await device.inputText(communityLink, new CommunityInput(device)); await device.clickOnElementAll(new JoinCommunityButton(device)); diff --git a/run/test/specs/utils/link_device.ts b/run/test/specs/utils/link_device.ts index d8761daf..f3dbf232 100644 --- a/run/test/specs/utils/link_device.ts +++ b/run/test/specs/utils/link_device.ts @@ -1,8 +1,15 @@ import { sleepFor } from '.'; import { newUser } from './create_account'; -import { DisplayNameInput, SeedPhraseInput } from '../locators/onboarding'; +import { + AccountRestoreButton, + DisplayNameInput, + SeedPhraseInput, + SlowModeRadio, +} from '../locators/onboarding'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import type { UserNameType } from '@session-foundation/qa-seeder'; +import { ContinueButton } from '../locators/global'; +import { PlusButton } from '../locators/home'; export const linkedDevice = async ( device1: DeviceWrapper, @@ -12,18 +19,17 @@ export const linkedDevice = async ( const user = await newUser(device1, userName); // Log in with recovery seed on device 2 - await device2.clickOnByAccessibilityID('Restore your session button'); + await device2.clickOnElementAll(new AccountRestoreButton(device2)); // Enter recovery phrase into input box await device2.inputText(user.recoveryPhrase, new SeedPhraseInput(device2)); - // Wait for continue button to become active await sleepFor(500); // Continue with recovery phrase - await device2.clickOnByAccessibilityID('Continue'); + await device2.clickOnElementAll(new ContinueButton(device2)); // Wait for any notifications to disappear - await device2.clickOnByAccessibilityID('Slow mode notifications button'); + await device2.clickOnElementAll(new SlowModeRadio(device2)); // Click continue on message notification settings - await device2.clickOnByAccessibilityID('Continue'); + await device2.clickOnElementAll(new ContinueButton(device2)); // Wait for loading animation to look for display name await device2.waitForLoadingOnboarding(); const displayName = await device2.doesElementExist({ @@ -32,7 +38,7 @@ export const linkedDevice = async ( }); if (displayName) { await device2.inputText(userName, new DisplayNameInput(device2)); - await device2.clickOnByAccessibilityID('Continue'); + await device2.clickOnElementAll(new ContinueButton(device2)); } else { console.info('Display name found: Loading account'); } @@ -40,10 +46,7 @@ export const linkedDevice = async ( await sleepFor(500); await device2.checkPermissions('Allow'); // Check that button was clicked - await device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'New conversation button', - }); + await device2.waitForTextElementToBePresent(new PlusButton(device2)); console.info('Device 2 linked'); diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index f7f3596b..f43b2d33 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -1,7 +1,9 @@ import { sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; -import { SeedPhraseInput } from '../locators/onboarding'; +import { AccountRestoreButton, SeedPhraseInput, SlowModeRadio } from '../locators/onboarding'; +import { ContinueButton } from '../../specs/locators/global'; +import { PlusButton } from '../locators/home'; export const restoreAccount = async (device: DeviceWrapper, user: User) => { await device.clickOnElementAll({ @@ -12,11 +14,11 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => { // Wait for continue button to become active await sleepFor(500); // Continue with recovery phrase - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // Wait for any notifications to disappear - await device.clickOnByAccessibilityID('Slow mode notifications button'); + await device.clickOnElementAll(new SlowModeRadio(device)); // Click continue on message notification settings - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // Wait for loading animation to look for display name await device.waitForLoadingOnboarding(); const displayName = await device.doesElementExist({ @@ -29,7 +31,7 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => { strategy: 'accessibility id', selector: 'Enter display name', }); - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); } else { console.info('Display name found: Loading account'); } @@ -38,15 +40,11 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => { await device.checkPermissions('Allow'); await sleepFor(1000); await device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Continue', + ...new ContinueButton(device).build(), maxWait: 1000, }); // Check that button was clicked - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'New conversation button', - }); + await device.waitForTextElementToBePresent(new PlusButton(device)); }; /** @@ -54,19 +52,16 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => { * If the account isn't found on the network, fail the test. */ export const restoreAccountNoFallback = async (device: DeviceWrapper, recoveryPhrase: string) => { - await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Restore your session button', - }); + await device.clickOnElementAll(new AccountRestoreButton(device)); await device.inputText(recoveryPhrase, new SeedPhraseInput(device)); // Wait for continue button to become active await sleepFor(500); // Continue with recovery phrase - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // Wait for any notifications to disappear - await device.clickOnByAccessibilityID('Slow mode notifications button'); + await device.clickOnElementAll(new SlowModeRadio(device)); // Click continue on message notification settings - await device.clickOnByAccessibilityID('Continue'); + await device.clickOnElementAll(new ContinueButton(device)); // Wait for loading animation to look for display name await device.waitForLoadingOnboarding(); const displayName = await device.doesElementExist({ @@ -84,13 +79,9 @@ export const restoreAccountNoFallback = async (device: DeviceWrapper, recoveryPh await device.checkPermissions('Allow'); await sleepFor(1000); await device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Continue', + ...new ContinueButton(device).build(), maxWait: 1000, }); // Check that button was clicked - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'New conversation button', - }); + await device.waitForTextElementToBePresent(new PlusButton(device)); }; diff --git a/run/test/specs/utils/set_disappearing_messages.ts b/run/test/specs/utils/set_disappearing_messages.ts index 6cd2e83a..89f3cb95 100644 --- a/run/test/specs/utils/set_disappearing_messages.ts +++ b/run/test/specs/utils/set_disappearing_messages.ts @@ -2,7 +2,10 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { ConversationType, DISAPPEARING_TIMES, MergedOptions } from '../../../types/testing'; import { ConversationSettings } from '../locators/conversation'; import { + DisappearingMessageRadial, DisappearingMessagesMenuOption, + DisappearingMessagesSubtitle, + DisappearingMessagesTimerType, FollowSettingsButton, SetDisappearMessagesButton, SetModalButton, @@ -22,7 +25,7 @@ export const setDisappearingMessage = async ( await sleepFor(500); await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); if (enforcedType === '1:1') { - await device.clickOnByAccessibilityID(timerType); + await device.clickOnElementAll(new DisappearingMessagesTimerType(device, timerType)); } if (timerType === 'Disappear after read option') { if (enforcedType === '1:1') { @@ -40,12 +43,9 @@ export const setDisappearingMessage = async ( await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); } - await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: timerDuration, - }); + await device.clickOnElementAll(new DisappearingMessageRadial(device, timerDuration)); await device.clickOnElementAll(new SetDisappearMessagesButton(device)); - await device.onIOS().navigateBack(); + await device.navigateBack(true); // Extended the wait for the Follow settings button to settle in the UI, it was moving and confusing appium await sleepFor(2000); if (device2) { @@ -53,5 +53,5 @@ export const setDisappearingMessage = async ( await sleepFor(500); await device2.clickOnElementAll(new SetModalButton(device2)); } - // await device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)); + await device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)); }; diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 7f1de13e..1427a7d2 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -1,6 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { ExitUserProfile } from './locators'; +import { CallButton, NotificationSettings, NotificationSwitch } from './locators/conversation'; import { open_Alice1_bob1_notfriends } from './state_builder'; import { sleepFor } from './utils/index'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -15,6 +16,7 @@ bothPlatformsItSeparate({ }, android: { testCb: voiceCallAndroid, + shouldSkip: true, }, }); @@ -26,11 +28,7 @@ async function voiceCallIos(platform: SupportedPlatformsType) { await alice1.sendNewMessage({ accountID: bob.sessionId }, 'Testing calls'); // Look for phone icon (shouldnt be there) - await alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Call', - maxWait: 1000, - }); + await alice1.hasElementBeenDeleted({ ...new CallButton(alice1).build(), maxWait: 5000 }); // Create contact await bob1.clickOnByAccessibilityID('Message requests banner'); // Select message from User A @@ -45,7 +43,7 @@ async function voiceCallIos(platform: SupportedPlatformsType) { const messageRequestsAccepted = englishStrippedStr('messageRequestsAccepted').toString(); await alice1.waitForControlMessageToBePresent(messageRequestsAccepted); // Phone icon should appear now that conversation has been approved - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Enabled voice calls in privacy settings await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', @@ -70,16 +68,17 @@ async function voiceCallIos(platform: SupportedPlatformsType) { await alice1.clickOnByAccessibilityID('Continue'); // Navigate back to conversation await alice1.closeScreen(); - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Need to allow microphone access await alice1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); // Call hasn't connected until microphone access is granted - await alice1.clickOnByAccessibilityID('Call'); - // No test tags on modal as of yet - // await bob1.checkModalStrings( - // englishStrippedStr('callsMissedCallFrom').withArgs({ name: alice.userName }).toString(), - // englishStrippedStr('callsYouMissedCallPermissions').withArgs({ name: alice.userName }).toString() - // ); + await alice1.clickOnElementAll(new CallButton(alice1)); + await bob1.checkModalStrings( + englishStrippedStr('callsMissedCallFrom').withArgs({ name: alice.userName }).toString(), + englishStrippedStr('callsYouMissedCallPermissions') + .withArgs({ name: alice.userName }) + .toString() + ); await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Okay' }); // Hang up on device 1 await alice1.clickOnByAccessibilityID('End call button'); @@ -89,7 +88,7 @@ async function voiceCallIos(platform: SupportedPlatformsType) { selector: 'Conversation list item', text: alice.userName, }); - await bob1.clickOnByAccessibilityID('Call'); + await bob1.clickOnElementAll(new CallButton(bob1)); await bob1.clickOnByAccessibilityID('Settings'); await bob1.scrollDown(); await bob1.modalPopup({ @@ -105,13 +104,19 @@ async function voiceCallIos(platform: SupportedPlatformsType) { englishStrippedStr('callsVoiceAndVideoModalDescription').toString() ); await bob1.clickOnByAccessibilityID('Continue'); - await bob1.clickOnElementAll(new ExitUserProfile(bob1)); + await bob1.checkModalStrings( + englishStrippedStr('sessionNotifications').toString(), + englishStrippedStr('callsNotificationsRequired').toString() + ); + await sleepFor(100); + await bob1.clickOnElementAll(new NotificationSettings(bob1)); + await bob1.clickOnElementAll({ ...new ExitUserProfile(bob1).build(), maxWait: 1000 }); // Wait for change to take effect await sleepFor(1000); // Make call on device 1 (alice) - await bob1.clickOnByAccessibilityID('Call'); + await bob1.clickOnElementAll(new CallButton(bob1)); await bob1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Wait for call to come through await sleepFor(1000); // Answer call on device 2 @@ -161,7 +166,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType) { // Verify config message states message request was accepted await alice1.waitForControlMessageToBePresent('Your message request has been accepted.'); // Phone icon should appear now that conversation has been approved - await alice1.clickOnByAccessibilityID('Call'); + await alice1.clickOnElementAll(new CallButton(alice1)); // Enabled voice calls in privacy settings await alice1.clickOnElementAll({ strategy: 'accessibility id', @@ -188,10 +193,17 @@ async function voiceCallAndroid(platform: SupportedPlatformsType) { await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - + await alice1.checkModalStrings( + englishStrippedStr('sessionNotifications').toString(), + englishStrippedStr('callsNotificationsRequired').toString(), + true + ); + await sleepFor(100); + await alice1.clickOnElementAll(new NotificationSettings(alice1)); + await alice1.clickOnElementAll(new NotificationSwitch(alice1)); await alice1.navigateBack(); // Enable voice calls on device 2 for User B - await bob1.clickOnByAccessibilityID('Call'); + await bob1.clickOnElementAll(new CallButton(bob1)); // Enabled voice calls in privacy settings await bob1.clickOnElementAll({ strategy: 'accessibility id', @@ -219,9 +231,18 @@ async function voiceCallAndroid(platform: SupportedPlatformsType) { await bob1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); + await bob1.checkModalStrings( + englishStrippedStr('sessionNotifications').toString(), + englishStrippedStr('callsNotificationsRequired').toString(), + true + ); + await sleepFor(100); + await bob1.clickOnElementAll(new NotificationSettings(bob1)); + await bob1.clickOnElementAll(new NotificationSwitch(bob1)); + await bob1.navigateBack(); await bob1.navigateBack(); // Make call on device 1 (alice) - await alice1.clickOnByAccessibilityID('Call'); + await bob1.clickOnElementAll(new CallButton(bob1)); // Answer call on device 2 await bob1.clickOnElementById('network.loki.messenger:id/acceptCallButton'); // Wait 5 seconds diff --git a/run/test/specs/warning_modal_new_account.spec.ts b/run/test/specs/warning_modal_new_account.spec.ts index 57a40895..966b3ab0 100644 --- a/run/test/specs/warning_modal_new_account.spec.ts +++ b/run/test/specs/warning_modal_new_account.spec.ts @@ -3,13 +3,13 @@ import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BackButton, - ContinueButton, CreateAccountButton, DisplayNameInput, SlowModeRadio, WarningModalQuitButton, } from './locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { ContinueButton } from '../specs/locators/global'; // These modals no longer exist in groups rebuild for iOS androidIt({ diff --git a/run/test/specs/warning_modal_restore_account.spec.ts b/run/test/specs/warning_modal_restore_account.spec.ts index 9b19597b..fa46a96a 100644 --- a/run/test/specs/warning_modal_restore_account.spec.ts +++ b/run/test/specs/warning_modal_restore_account.spec.ts @@ -3,12 +3,12 @@ import { androidIt } from '../../types/sessionIt'; import { AccountRestoreButton, BackButton, - ContinueButton, SeedPhraseInput, SlowModeRadio, WarningModalQuitButton, } from './locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { ContinueButton } from '../specs/locators/global'; // These modals no longer exist in groups rebuild for iOS androidIt({ title: 'Warning modal on restore account', diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 60cad71c..7d9b68c3 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -10,14 +10,29 @@ import { ImageName, ImagePermissionsModalAllow, LocatorsInterface, - PrivacyButton, ReadReceiptsButton, SendMediaButton, } from '../../run/test/specs/locators'; import { IOS_XPATHS } from '../constants'; +import { profilePicture, testFile, testImage, testVideo } from '../constants/testfiles'; +import { englishStrippedStr } from '../localizer/englishStrippedStr'; +import { + AttachmentsButton, + MessageInput, + OutgoingMessageStatusSent, +} from '../test/specs/locators/conversation'; import { ModalDescription, ModalHeading } from '../test/specs/locators/global'; -import { SaveProfilePictureButton, UserSettings } from '../test/specs/locators/settings'; -import { EnterAccountID } from '../test/specs/locators/start_conversation'; +import { LoadingAnimation } from '../test/specs/locators/onboarding'; +import { + PrivacyMenuItem, + SaveProfilePictureButton, + UserSettings, +} from '../test/specs/locators/settings'; +import { + EnterAccountID, + NewMessageOption, + NextButton, +} from '../test/specs/locators/start_conversation'; import { clickOnCoordinates, sleepFor } from '../test/specs/utils'; import { getAdbFullPath } from '../test/specs/utils/binaries'; import { parseDataImage } from '../test/specs/utils/check_colour'; @@ -34,9 +49,7 @@ import { User, XPath, } from './testing'; -import { testFile, testImage, testVideo, profilePicture } from '../constants/testfiles'; -import { AttachmentsButton, OutgoingMessageStatusSent } from '../test/specs/locators/conversation'; -import { englishStrippedStr } from '../localizer/englishStrippedStr'; +import { PlusButton } from '../test/specs/locators/home'; export type Coordinates = { x: number; @@ -913,8 +926,7 @@ export class DeviceWrapper { do { try { loadingAnimation = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Loading animation', + ...new LoadingAnimation(this).build(), maxWait: 1000, }); @@ -975,17 +987,17 @@ export class DeviceWrapper { public async sendNewMessage(user: Pick, message: string) { // Sender workflow // Click on plus button - await this.clickOnByAccessibilityID('New conversation button'); + await this.clickOnElementAll(new PlusButton(this)); // Select direct message option - await this.clickOnByAccessibilityID('New direct message'); + await this.clickOnElementAll(new NewMessageOption(this)); // Enter User B's session ID into input box await this.inputText(user.accountID, new EnterAccountID(this)); // Click next await this.scrollDown(); - await this.clickOnByAccessibilityID('Next'); + await this.clickOnElementAll(new NextButton(this)); // Type message into message input box - await this.inputText(message, { strategy: 'accessibility id', selector: 'Message input box' }); + await this.inputText(message, new MessageInput(this)); // Click send const sendButton = await this.clickOnElementAll({ strategy: 'accessibility id', @@ -1094,7 +1106,7 @@ export class DeviceWrapper { } } else { const radioButton = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', + strategy: 'id', selector: timeOption, }); const attr = await this.getAttribute('selected', radioButton.ELEMENT); @@ -1637,7 +1649,7 @@ export class DeviceWrapper { await sleepFor(100); await this.clickOnElementAll(new UserSettings(this)); await sleepFor(500); - await this.clickOnElementAll(new PrivacyButton(this)); + await this.clickOnElementAll(new PrivacyMenuItem(this)); await sleepFor(2000); await this.clickOnElementAll(new ReadReceiptsButton(this)); await this.navigateBack(); diff --git a/run/types/testing.ts b/run/types/testing.ts index 49b0b5be..78ea37a7 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -77,6 +77,8 @@ export enum DISAPPEARING_TIMES { OFF_ANDROID = 'Disable disappearing messages', } +export type DisappearingOptions = `Disappear after ${DisappearModes} option`; + export type DisappearOpts1o1 = [ '1:1', `Disappear after ${DisappearModes} option`, @@ -351,7 +353,9 @@ export type AccessibilityId = | 'Legacy Groups Recreate Button' | 'Confirm leave' | 'Photo, 25 March, 11:09 am' - | 'Albums'; + | 'Albums' + | `Disappear after ${DisappearActions} option` + | 'Call button'; export type Id = | 'Modal heading' @@ -387,7 +391,6 @@ export type Id = | 'Delete' | 'android:id/content_preview_text' | 'network.loki.messenger:id/search_result_title' - | 'Error message' | 'Enter display name' | 'Session id input box' | 'com.android.chrome:id/url_bar' @@ -418,7 +421,37 @@ export type Id = | 'Remove' | 'Contact status' | 'Image button' - | 'android.widget.TextView'; + | 'android.widget.TextView' + | 'Create account button' + | 'Restore your session button' + | 'Open URL' + | 'Loading animation' + | 'Slow mode notifications button' + | 'Reveal recovery phrase button' + | 'Recovery password container' + | 'Copy button' + | 'New direct message' + | 'Join community button' + | 'Invite friend button' + | 'Conversations' + | 'Hide recovery password button' + | 'error-message' + | 'Next' + | 'Set button' + | 'disappearing-messages-menu-option' + | 'Disable disappearing messages' + | DISAPPEARING_TIMES + | 'conversation-options-avatar' + | `Disappear after ${DisappearModes} option` + | 'Disappearing messages type and time' + | 'Account ID' + | 'Share button' + | 'Call' + | 'Conversation header name' + | 'block-user-confirm-button' + | 'Notifications' + | 'All Session notifications' + | 'com.android.settings:id/switch_text'; export type TestRisk = 'high' | 'medium' | 'low'; From 1ba6992ac7813b1212c0c40c06efc047fbb7b2f3 Mon Sep 17 00:00:00 2001 From: wafflesvsfrankie <92288602+burtonemily@users.noreply.github.com> Date: Thu, 29 May 2025 17:45:23 +1000 Subject: [PATCH 02/16] lint fix --- run/test/specs/linked_device_block_user.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 075e6fc1..fb2e0f39 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -45,7 +45,7 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) }); if (blockedStatus) { // Check linked device for blocked status (if shown on alice1) - await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName)) + await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName)); await alice2.onAndroid().waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Blocked banner', From 1e6bb123df76e435b17614d64cd5c1c2a40109e3 Mon Sep 17 00:00:00 2001 From: wafflesvsfrankie <92288602+burtonemily@users.noreply.github.com> Date: Thu, 29 May 2025 17:46:22 +1000 Subject: [PATCH 03/16] pull latest --- run/test/specs/message_requests_accept_text_reply.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/run/test/specs/message_requests_accept_text_reply.spec.ts b/run/test/specs/message_requests_accept_text_reply.spec.ts index 0a05fc18..da4a0c68 100644 --- a/run/test/specs/message_requests_accept_text_reply.spec.ts +++ b/run/test/specs/message_requests_accept_text_reply.spec.ts @@ -1,6 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { MessageInput } from './locators/conversation'; import { PlusButton } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; From 8d81aba365755bfd927f77587631f093af45c611 Mon Sep 17 00:00:00 2001 From: wafflesvsfrankie <92288602+burtonemily@users.noreply.github.com> Date: Thu, 29 May 2025 17:47:10 +1000 Subject: [PATCH 04/16] lint fix --- run/types/testing.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/run/types/testing.ts b/run/types/testing.ts index d068aaf7..a22d88b4 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -362,8 +362,7 @@ export type AccessibilityId = | 'Learn more link' | 'Open' | 'Learn about staking link' - | 'Last updated timestamp' - | 'Albums'; + | 'Last updated timestamp'; export type Id = | 'Modal heading' From b371efe97208b57f4c9ecb8b5b53f4b5763899e8 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 13:48:11 +1000 Subject: [PATCH 05/16] chore: update app disguise screenshot --- run/screenshots/ios/app_disguise.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/screenshots/ios/app_disguise.png b/run/screenshots/ios/app_disguise.png index 1fc32595..92627131 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:78a8d6e3e8dd344b8a0f1fdd711ec5b0f63db546929c113e0877ac72fadb1bb5 -size 477646 +oid sha256:74c3f2a2e564e6e78c245d2fd5cf410e930c76672a839dedf85e5177a79ed5ea +size 476984 From d59ebc8c9e5c9c97e4d86da5bbb384263e8d7ae0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 14:39:25 +1000 Subject: [PATCH 06/16] feat: add BlockedBanner locator and update related tests --- run/test/specs/locators/conversation.ts | 9 +++++++++ run/test/specs/locators/index.ts | 2 +- .../user_actions_block_conversation_list.spec.ts | 6 ++---- ...er_actions_block_conversation_options.spec.ts | 7 ++----- run/test/specs/user_actions_unblock_user.spec.ts | 16 ++++------------ run/types/DeviceWrapper.ts | 1 + run/types/testing.ts | 1 - 7 files changed, 19 insertions(+), 23 deletions(-) diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 28eba000..4c164ebf 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -161,3 +161,12 @@ export class NotificationSwitch extends LocatorsInterface { } } } + +export class BlockedBanner extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'Blocked banner', + } as const; + } +} \ No newline at end of file diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 8ba958da..ffba1c99 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -204,7 +204,7 @@ export class BlockUser extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Block - Switch', + selector: 'Block', }; case 'android': return { 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 5d77fb3f..cd98903f 100644 --- a/run/test/specs/user_actions_block_conversation_list.spec.ts +++ b/run/test/specs/user_actions_block_conversation_list.spec.ts @@ -6,6 +6,7 @@ import { LongPressBlockOption } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; +import { BlockedBanner } from './locators/conversation'; // Block option no longer available on iOS in conversation list androidIt({ @@ -38,10 +39,7 @@ async function blockUserInConversationList(platform: SupportedPlatformsType) { selector: 'Conversation list item', text: bob.userName, }); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Blocked banner', - }); + await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); await alice1.navigateBack(); await alice1.clickOnElementAll(new UserSettings(alice1)); // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it 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 688e792f..a73bffad 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -6,7 +6,7 @@ import { BlockUserConfirmationModal, ExitUserProfile, } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationSettings, BlockedBanner } from './locators/conversation'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -46,10 +46,7 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) await alice1.onIOS().navigateBack(); // Look for alert at top of screen (Bob is blocked. Unblock them?) // Check device 1 for blocked status - const blockedStatus = await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Blocked banner', - }); + const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); if (blockedStatus) { console.info(`${bob.userName} has been blocked`); } else { diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index 52c2a2a2..036a4607 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -1,7 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockUser, BlockUserConfirmationModal } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { BlockedBanner, ConversationSettings } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType } from './utils/open_app'; @@ -30,11 +30,7 @@ async function unblockUser(platform: SupportedPlatformsType) { ); await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); await alice1.onIOS().navigateBack(); - const blockedStatus = await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Blocked banner', - }); - + const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); if (blockedStatus) { console.info(`${bob.userName} has been blocked`); } else { @@ -49,16 +45,12 @@ async function unblockUser(platform: SupportedPlatformsType) { maxWait: 5000, }); // Now that user is blocked, unblock them - await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Blocked banner' }); + await alice1.clickOnElementAll(new BlockedBanner(alice1)); await alice1.checkModalStrings( englishStrippedStr('blockUnblock').toString(), englishStrippedStr('blockUnblockName').withArgs({ name: bob.userName }).toString(), true ); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Unblock' }); - await alice1.doesElementExist({ - strategy: 'accessibility id', - selector: 'Blocked banner', - maxWait: 2000, - }); + await alice1.doesElementExist({...new BlockedBanner(alice1).build(), maxWait: 2000}); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 69a84f17..8251d41b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1539,6 +1539,7 @@ export class DeviceWrapper { // Push file first await this.pushMediaToDevice(profilePicture); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); + await sleepFor(1000); await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, profilePicture diff --git a/run/types/testing.ts b/run/types/testing.ts index 843d27f4..7d9c809c 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -235,7 +235,6 @@ export type AccessibilityId = | 'Confirm delete' | 'Delete' | 'Block' - | 'Block - Switch' | 'Unblock' | 'Confirm block' | 'Blocked contacts' From ae19cab647065048f2c6d73477fa7ca1ab63b761 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 15:15:36 +1000 Subject: [PATCH 07/16] feat: generalize handle_first_open --- run/test/specs/donate.spec.ts | 2 +- run/test/specs/linked_device_block_user.spec.ts | 12 +++--------- run/test/specs/locators/conversation.ts | 2 +- run/test/specs/locators/external.ts | 15 +++++++++++++++ run/test/specs/network_page_link_network.spec.ts | 2 +- run/test/specs/network_page_link_staking.spec.ts | 2 +- run/test/specs/onboarding_pp.spec.ts | 2 +- run/test/specs/onboarding_tos.spec.ts | 2 +- .../specs/user_actions_share_to_session.spec.ts | 14 ++++++++++---- run/test/specs/user_actions_unblock_user.spec.ts | 2 +- ...me_first_time_open.ts => handle_first_open.ts} | 13 +++++++++++++ run/types/testing.ts | 3 ++- 12 files changed, 50 insertions(+), 21 deletions(-) rename run/test/specs/utils/{chrome_first_time_open.ts => handle_first_open.ts} (56%) diff --git a/run/test/specs/donate.spec.ts b/run/test/specs/donate.spec.ts index b414eafd..1a0ee158 100644 --- a/run/test/specs/donate.spec.ts +++ b/run/test/specs/donate.spec.ts @@ -3,7 +3,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { SafariAddressBar, URLInputField } from './locators/browsers'; import { DonationsMenuItem, UserSettings } from './locators/settings'; -import { handleChromeFirstTimeOpen } from './utils/chrome_first_time_open'; +import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { ensureHttpsURL } from './utils/utilities'; diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 493c7fbc..13d80956 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -1,7 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationSettings, BlockedBanner } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice2_Bob1_friends } from './state_builder'; @@ -39,17 +39,11 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) await alice1.onIOS().navigateBack(); // Look for alert at top of screen (Bob is blocked. Unblock them?) // Check device 1 for blocked status - const blockedStatus = await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Blocked banner', - }); + const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); if (blockedStatus) { // Check linked device for blocked status (if shown on alice1) await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName)); - await alice2.onAndroid().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Blocked banner', - }); + await alice2.onAndroid().waitForTextElementToBePresent(new BlockedBanner(alice2)); console.info(`${bob.userName}` + ' has been blocked'); } else { console.info('Blocked banner not found'); diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 4c164ebf..97549dd9 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -169,4 +169,4 @@ export class BlockedBanner extends LocatorsInterface { selector: 'Blocked banner', } as const; } -} \ No newline at end of file +} diff --git a/run/test/specs/locators/external.ts b/run/test/specs/locators/external.ts index 1be7b413..a6b7252a 100644 --- a/run/test/specs/locators/external.ts +++ b/run/test/specs/locators/external.ts @@ -57,3 +57,18 @@ export class IOSReplaceButton extends LocatorsInterface { } } } + +export class iOSPhotosContinuebutton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + throw new Error('Unsupported platform'); + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeButton[@name="Continue"]`, + maxWait: 5000, + } as const; + } + } +} diff --git a/run/test/specs/network_page_link_network.spec.ts b/run/test/specs/network_page_link_network.spec.ts index 8287bc3f..5df5c9cf 100644 --- a/run/test/specs/network_page_link_network.spec.ts +++ b/run/test/specs/network_page_link_network.spec.ts @@ -8,7 +8,7 @@ import { SessionNetworkMenuItem, } from './locators/network_page'; import { UserSettings } from './locators/settings'; -import { handleChromeFirstTimeOpen } from './utils/chrome_first_time_open'; +import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { ensureHttpsURL } from './utils/utilities'; diff --git a/run/test/specs/network_page_link_staking.spec.ts b/run/test/specs/network_page_link_staking.spec.ts index 13bd5fef..dadfc03c 100644 --- a/run/test/specs/network_page_link_staking.spec.ts +++ b/run/test/specs/network_page_link_staking.spec.ts @@ -9,7 +9,7 @@ import { SessionNetworkMenuItem, } from './locators/network_page'; import { UserSettings } from './locators/settings'; -import { handleChromeFirstTimeOpen } from './utils/chrome_first_time_open'; +import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { ensureHttpsURL } from './utils/utilities'; diff --git a/run/test/specs/onboarding_pp.spec.ts b/run/test/specs/onboarding_pp.spec.ts index 5b1be478..9e71e4cf 100644 --- a/run/test/specs/onboarding_pp.spec.ts +++ b/run/test/specs/onboarding_pp.spec.ts @@ -1,7 +1,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { SafariAddressBar, URLInputField } from './locators/browsers'; import { PrivacyPolicyButton, SplashScreenLinks } from './locators/onboarding'; -import { handleChromeFirstTimeOpen } from './utils/chrome_first_time_open'; +import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { ensureHttpsURL } from './utils/utilities'; diff --git a/run/test/specs/onboarding_tos.spec.ts b/run/test/specs/onboarding_tos.spec.ts index 384582f6..6979f6d2 100644 --- a/run/test/specs/onboarding_tos.spec.ts +++ b/run/test/specs/onboarding_tos.spec.ts @@ -1,7 +1,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { TermsOfServiceButton, SplashScreenLinks } from './locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { handleChromeFirstTimeOpen } from './utils/chrome_first_time_open'; +import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { URLInputField, SafariAddressBar } from './locators/browsers'; import { ensureHttpsURL } from './utils/utilities'; 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 2bac498c..84b7e216 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -6,6 +6,7 @@ import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { SupportedPlatformsType } from './utils/open_app'; import { testImage } from '../../constants/testfiles'; +import { handlePhotosFirstTimeOpen } from './utils/handle_first_open'; bothPlatformsIt({ title: 'Share to session', @@ -32,10 +33,15 @@ async function shareToSession(platform: SupportedPlatformsType) { await alice1.onIOS().swipeRightAny('Session'); await alice1.clickOnElementAll(new PhotoLibrary(alice1)); await sleepFor(2000); - await alice1.onIOS().clickOnByAccessibilityID('Select'); - await alice1 - .onIOS() - .matchAndTapImage({ strategy: 'xpath', selector: `//XCUIElementTypeImage` }, testImage); + if (platform === 'ios') { + // first launch of Photos app on iOS shows a 'What's New' screen + await handlePhotosFirstTimeOpen(alice1); + await alice1.clickOnByAccessibilityID('Select'); + await alice1.matchAndTapImage( + { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, + testImage + ); + } await alice1.onAndroid().clickOnElementAll(new ImageName(alice1)); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Share' }); await alice1.clickOnElementAll(new ShareExtensionIcon(alice1)); diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index 036a4607..50ebff2d 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -52,5 +52,5 @@ async function unblockUser(platform: SupportedPlatformsType) { true ); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Unblock' }); - await alice1.doesElementExist({...new BlockedBanner(alice1).build(), maxWait: 2000}); + await alice1.doesElementExist({ ...new BlockedBanner(alice1).build(), maxWait: 2000 }); } diff --git a/run/test/specs/utils/chrome_first_time_open.ts b/run/test/specs/utils/handle_first_open.ts similarity index 56% rename from run/test/specs/utils/chrome_first_time_open.ts rename to run/test/specs/utils/handle_first_open.ts index 296687a4..72cdbdd3 100644 --- a/run/test/specs/utils/chrome_first_time_open.ts +++ b/run/test/specs/utils/handle_first_open.ts @@ -1,5 +1,6 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { ChromeNotificationsNegativeButton, ChromeUseWithoutAnAccount } from '../locators/browsers'; +import { iOSPhotosContinuebutton } from '../locators/external'; // First time open of Chrome triggers an account check and a notifications modal export async function handleChromeFirstTimeOpen(device: DeviceWrapper) { @@ -16,3 +17,15 @@ export async function handleChromeFirstTimeOpen(device: DeviceWrapper) { await device.clickOnElementAll(new ChromeNotificationsNegativeButton(device)); } } + +// First time Photos.app open triggers a "What's New" and a permissions modal +export async function handlePhotosFirstTimeOpen(device: DeviceWrapper) { + const continueButton = await device.doesElementExist(new iOSPhotosContinuebutton(device)); + if (!continueButton) { + console.log(`Photos app opened without a "What's New" screen, proceeding`); + } else { + console.log(`Photos app has been opened for the first time, dismissing modals`); + await device.clickOnElementAll(new iOSPhotosContinuebutton(device)); + await device.clickOnByAccessibilityID('Don’t Allow'); + } +} diff --git a/run/types/testing.ts b/run/types/testing.ts index 7d9c809c..6e1cca1d 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -141,7 +141,8 @@ export type XPath = | `//*[starts-with(@content-desc, "Photo taken on")]` | `//XCUIElementTypeImage` | '//XCUIElementTypeCell' - | `(//android.widget.ImageView[@resource-id="network.loki.messenger:id/thumbnail"])[1]`; + | `(//android.widget.ImageView[@resource-id="network.loki.messenger:id/thumbnail"])[1]` + | `//XCUIElementTypeButton[@name="Continue"]`; export type AccessibilityId = | 'Create account button' From be45895d7de543188617aaaa54b7ee01789025de Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:02:34 +1000 Subject: [PATCH 08/16] fix: scale match coordinates before tapping in matchAndTapImage --- run/types/DeviceWrapper.ts | 41 ++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 8251d41b..61707ac4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -769,11 +769,44 @@ export class DeviceWrapper { const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { threshold, }); + // Calculate scale between resized image and element dimensions + const resizedMeta = await sharp(resizedRef).metadata(); + const scaleX = rect.width / (resizedMeta.width ?? rect.width); + const scaleY = rect.height / (resizedMeta.height ?? rect.height); + + // Calculate center of the match rectangle (in buffer space) + const matchCenterX = matchRect.x + Math.floor(matchRect.width / 2); + const matchCenterY = matchRect.y + Math.floor(matchRect.height / 2); + + // Scale match center down to element space + const scaledCenterX = matchCenterX * scaleX; + const scaledCenterY = matchCenterY * scaleY; + + // Final absolute coordinates + const tapX = Math.round(rect.x + scaledCenterX); + const tapY = Math.round(rect.y + scaledCenterY); + + console.info( + `[matchAndTapImageDEBUG] Screenshot meta: ${resizedMeta.width}x${resizedMeta.height}` + ); + console.info( + `[matchAndTapImageDEBUG] Element ${i + 1}: rect = x:${rect.x}, y:${rect.y}, width:${rect.width}, height:${rect.height}` + ); + console.info( + `[matchAndTapImageDEBUG] Match rect = x:${matchRect.x}, y:${matchRect.y}, width:${matchRect.width}, height:${matchRect.height}` + ); + console.info( + `[matchAndTapImageDEBUG] Scale factors: scaleX=${scaleX.toFixed(4)}, scaleY=${scaleY.toFixed(4)}` + ); + console.info( + `[matchAndTapImageDEBUG] Match center in image space: (${matchCenterX}, ${matchCenterY})` + ); + console.info( + `[matchAndTapImageDEBUG] Scaled center: (${scaledCenterX.toFixed(2)}, ${scaledCenterY.toFixed(2)})` + ); + console.info(`[matchAndTapImageDEBUG] Raw tap coordinates: x:${tapX}, y:${tapY}`); console.info(`[matchAndTapImage] Match score for element ${i + 1}: ${score.toFixed(4)}`); - const center = { - x: rect.x + matchRect.x + Math.floor(matchRect.width / 2), - y: rect.y + matchRect.y + Math.floor(matchRect.height / 2), - }; + const center = { x: tapX, y: tapY }; // If earlyMatch is enabled and the score is high enough, tap immediately if (earlyMatch && score >= earlyMatchThreshold) { From ad59ca2f89a80e40c18213230b270f50b86f109f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:11:40 +1000 Subject: [PATCH 09/16] chore: remove debug logging --- run/types/DeviceWrapper.ts | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 61707ac4..3466baab 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -769,6 +769,17 @@ export class DeviceWrapper { const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { threshold, }); + console.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. + * However, the coordinates returned by getImageOccurrence are relative to the resized buffer, + * *not* the original screen element. This leads to incorrect tap positions unless we + * scale the match result back down to the actual dimensions of the element. + * The logic below handles this scaling correction, ensuring the tap lands at the correct + * screen coordinates — even when Retina displays and image resizing are involved. + */ + // Calculate scale between resized image and element dimensions const resizedMeta = await sharp(resizedRef).metadata(); const scaleX = rect.width / (resizedMeta.width ?? rect.width); @@ -786,26 +797,6 @@ export class DeviceWrapper { const tapX = Math.round(rect.x + scaledCenterX); const tapY = Math.round(rect.y + scaledCenterY); - console.info( - `[matchAndTapImageDEBUG] Screenshot meta: ${resizedMeta.width}x${resizedMeta.height}` - ); - console.info( - `[matchAndTapImageDEBUG] Element ${i + 1}: rect = x:${rect.x}, y:${rect.y}, width:${rect.width}, height:${rect.height}` - ); - console.info( - `[matchAndTapImageDEBUG] Match rect = x:${matchRect.x}, y:${matchRect.y}, width:${matchRect.width}, height:${matchRect.height}` - ); - console.info( - `[matchAndTapImageDEBUG] Scale factors: scaleX=${scaleX.toFixed(4)}, scaleY=${scaleY.toFixed(4)}` - ); - console.info( - `[matchAndTapImageDEBUG] Match center in image space: (${matchCenterX}, ${matchCenterY})` - ); - console.info( - `[matchAndTapImageDEBUG] Scaled center: (${scaledCenterX.toFixed(2)}, ${scaledCenterY.toFixed(2)})` - ); - console.info(`[matchAndTapImageDEBUG] Raw tap coordinates: x:${tapX}, y:${tapY}`); - console.info(`[matchAndTapImage] Match score for element ${i + 1}: ${score.toFixed(4)}`); const center = { x: tapX, y: tapY }; // If earlyMatch is enabled and the score is high enough, tap immediately From 3aeebabe4fa5aa13afe8e0a93722dd35072ee9de Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:18:55 +1000 Subject: [PATCH 10/16] fix: wait longer for media picker to show --- run/types/DeviceWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 3466baab..7130efa8 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1563,7 +1563,7 @@ export class DeviceWrapper { // Push file first await this.pushMediaToDevice(profilePicture); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - await sleepFor(1000); + await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, profilePicture From eb4c2a150986d7e5718b9ffc60387a740cc774aa Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:39:16 +1000 Subject: [PATCH 11/16] fix: update modal strings for group name change confirmation --- run/test/specs/group_tests_change_group_name.spec.ts | 4 ++-- run/test/specs/linked_device_create_group.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run/test/specs/group_tests_change_group_name.spec.ts b/run/test/specs/group_tests_change_group_name.spec.ts index 436a9fe9..b7b19c0a 100644 --- a/run/test/specs/group_tests_change_group_name.spec.ts +++ b/run/test/specs/group_tests_change_group_name.spec.ts @@ -38,8 +38,8 @@ async function changeGroupNameIos(platform: SupportedPlatformsType) { // Click on current group name await alice1.clickOnElementAll(new EditGroupName(alice1)); await alice1.checkModalStrings( - englishStrippedStr(`groupInformationSet`).toString(), - englishStrippedStr(`groupNameVisible`).toString() + englishStrippedStr(`updateGroupInformation`).toString(), + englishStrippedStr(`updateGroupInformationDescription`).toString() ); await alice1.deleteText(new EditGroupNameInput(alice1)); await alice1.inputText(' ', new EditGroupNameInput(alice1)); diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 6fc851e9..8b8b813a 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -46,8 +46,8 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { await device1.clickOnElementAll(new EditGroupName(device1)); // Check new dialog await device1.checkModalStrings( - englishStrippedStr(`groupInformationSet`).toString(), - englishStrippedStr(`groupNameVisible`).toString() + englishStrippedStr(`updateGroupInformation`).toString(), + englishStrippedStr(`updateGroupInformationDescription`).toString() ); // Delete old name first await device1.deleteText(new EditGroupNameInput(device1)); From e5df3640e20c60a009e4e126bcb5ceb604ace721 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:39:52 +1000 Subject: [PATCH 12/16] fix: add maxWait to ErrorMessage locator --- run/test/specs/locators/onboarding.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run/test/specs/locators/onboarding.ts b/run/test/specs/locators/onboarding.ts index a9ad3e28..b139591e 100644 --- a/run/test/specs/locators/onboarding.ts +++ b/run/test/specs/locators/onboarding.ts @@ -10,11 +10,13 @@ export class ErrorMessage extends LocatorsInterface { return { strategy: 'id', selector: 'error-message', + maxWait: 5000, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Error message', + maxWait: 5000, } as const; } } From 2f1a9501870192985ad9feb94b6adb014154bb17 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Jun 2025 16:39:58 +1000 Subject: [PATCH 13/16] chore: yarn lint --- run/types/DeviceWrapper.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 7130efa8..32723938 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -771,14 +771,14 @@ export class DeviceWrapper { }); console.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. - * However, the coordinates returned by getImageOccurrence are relative to the resized buffer, - * *not* the original screen element. This leads to incorrect tap positions unless we - * scale the match result back down to the actual dimensions of the element. - * The logic below handles this scaling correction, ensuring the tap lands at the correct - * screen coordinates — even when Retina displays and image resizing are involved. - */ + /** + * Matching is done on a resized reference image to account for device pixel density. + * However, the coordinates returned by getImageOccurrence are relative to the resized buffer, + * *not* the original screen element. This leads to incorrect tap positions unless we + * scale the match result back down to the actual dimensions of the element. + * The logic below handles this scaling correction, ensuring the tap lands at the correct + * screen coordinates — even when Retina displays and image resizing are involved. + */ // Calculate scale between resized image and element dimensions const resizedMeta = await sharp(resizedRef).metadata(); @@ -1563,7 +1563,7 @@ export class DeviceWrapper { // Push file first await this.pushMediaToDevice(profilePicture); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately + await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, profilePicture From 64c1623068a27aced7a30a1a9145151314d68ad2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Jun 2025 13:17:34 +1000 Subject: [PATCH 14/16] fix: extend DEVICE_PORT_RANGE for improved port allocation --- patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch | 4 ++-- yarn.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch b/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch index aa16fd1a..276fa531 100644 --- a/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch +++ b/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch @@ -1,5 +1,5 @@ diff --git a/build/lib/driver.js b/build/lib/driver.js -index 4287b4fc525093dee083bd37869947902bcc0398..3c8c6ab0b8c08ab6519268465e3c5bfd06068f86 100644 +index 4287b4fc525093dee083bd37869947902bcc0398..dc1536388c2c9782c382509a9d6c0968d793b1b7 100644 --- a/build/lib/driver.js +++ b/build/lib/driver.js @@ -60,7 +60,7 @@ const screenshot_1 = require("./commands/screenshot"); @@ -7,7 +7,7 @@ index 4287b4fc525093dee083bd37869947902bcc0398..3c8c6ab0b8c08ab6519268465e3c5bfd // The range of ports we can use on the system for communicating to the // UiAutomator2 HTTP server on the device -const DEVICE_PORT_RANGE = [8200, 8299]; -+const DEVICE_PORT_RANGE = [8200, 8399]; ++const DEVICE_PORT_RANGE = [8200, 8499]; // The guard is needed to avoid dynamic system port allocation conflicts for // parallel driver sessions const DEVICE_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'uia2_device_port_guard'), { timeout: 25, tryRecovery: true }); diff --git a/yarn.lock b/yarn.lock index 95110b6c..03b9539f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1686,7 +1686,7 @@ __metadata: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch": version: 3.8.2 - resolution: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch::version=3.8.2&hash=a0d581" + resolution: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch::version=3.8.2&hash=84490b" dependencies: appium-adb: "npm:^12.7.0" appium-android-driver: "npm:^9.12.3" @@ -1703,7 +1703,7 @@ __metadata: type-fest: "npm:^4.4.0" peerDependencies: appium: ^2.4.1 - checksum: 10c0/a026f90c2089a1c4fd7a8667d0b19999aa60297b36480d79d8056e8dd80e5da1177cd992ccc029350131ff2f442d9686136ce10c0e2ef1dcc175ea0b9a4e8bfb + checksum: 10c0/b1c7b9447a67acf9424a164bfa53329693e9b4d532c8b61c08325aab6793ef52633781c08f16d6e0aac0731c49526b76edd02ae9ce1eb756461ec1b51c9699e1 languageName: node linkType: hard From 342b91d8586a4dcfdb065715677a314532fc4d42 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Jun 2025 16:24:13 +1000 Subject: [PATCH 15/16] feat: implement fallback locator logic for new and old Android --- .../specs/group_tests_add_contact.spec.ts | 2 +- .../group_tests_change_group_name.spec.ts | 2 +- .../specs/group_tests_kick_member.spec.ts | 2 +- .../specs/linked_device_create_group.spec.ts | 2 +- run/test/specs/locators/index.ts | 18 ++- ...actions_block_conversation_options.spec.ts | 11 +- .../specs/user_actions_set_nickname.spec.ts | 2 +- run/test/specs/utils/create_group.ts | 8 +- .../specs/utils/set_disappearing_messages.ts | 2 +- run/types/DeviceWrapper.ts | 153 ++++++++++++------ run/types/testing.ts | 4 +- 11 files changed, 139 insertions(+), 67 deletions(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index c26879dd..66eeecec 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -51,7 +51,7 @@ async function addContactToGroup(platform: SupportedPlatformsType) { // Click done/apply await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); // Click done/apply again - await alice1.navigateBack(true); + await alice1.navigateBack(); // iOS doesn't automatically go back to conversation settings await alice1.onIOS().navigateBack(); // Check control messages diff --git a/run/test/specs/group_tests_change_group_name.spec.ts b/run/test/specs/group_tests_change_group_name.spec.ts index b7b19c0a..fb28a07c 100644 --- a/run/test/specs/group_tests_change_group_name.spec.ts +++ b/run/test/specs/group_tests_change_group_name.spec.ts @@ -90,7 +90,7 @@ async function changeGroupNameAndroid(platform: SupportedPlatformsType) { await alice1.inputText(newGroupName, new EditGroupName(alice1)); // Click done/apply await alice1.clickOnByAccessibilityID('Confirm'); - await alice1.navigateBack(true); + await alice1.navigateBack(); // Check control message for changed name await alice1.waitForControlMessageToBePresent( englishStrippedStr('groupNameNew').withArgs({ group_name: newGroupName }).toString() diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index 39191edf..d8713d7f 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -45,7 +45,7 @@ async function kickMember(platform: SupportedPlatformsType) { ...new GroupMember(alice1).build(USERNAME.BOB), maxWait: 10000, }); - await alice1.navigateBack(true); + await alice1.navigateBack(); await alice1.onIOS().navigateBack(); await Promise.all([ alice1.waitForControlMessageToBePresent( diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 8b8b813a..a6e36a13 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -106,7 +106,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType) { await device1.inputText(newGroupName, new EditGroupNameInput(device1)); // Click done/apply await device1.clickOnByAccessibilityID('Confirm'); - await device1.navigateBack(true); + await device1.navigateBack(); // Check control message for changed name const groupNameNew = englishStrippedStr('groupNameNew') .withArgs({ group_name: newGroupName }) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index ffba1c99..c23579b9 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -209,7 +209,7 @@ export class BlockUser extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'block-user-confirm-button', + selector: 'block-user-menu-option', }; } } @@ -386,10 +386,18 @@ export class LeaveGroup extends LocatorsInterface { export class BlockUserConfirmationModal extends LocatorsInterface { public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Block', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Block', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Block', + } as const; + } } } 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 a73bffad..4e70079a 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -36,14 +36,13 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) // Check modal strings await alice1.checkModalStrings( englishStrippedStr('block').toString(), - englishStrippedStr('blockDescription').withArgs({ name: bob.userName }).toString(), - true + englishStrippedStr('blockDescription').withArgs({ name: bob.userName }).toString() ); // Confirm block option await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); await sleepFor(1000); - // On ios, you need to navigate back to conversation screen to confirm block - await alice1.onIOS().navigateBack(); + // Navigate back to conversation screen to confirm block + await alice1.navigateBack(); // Look for alert at top of screen (Bob is blocked. Unblock them?) // Check device 1 for blocked status const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); @@ -65,8 +64,8 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) selector: 'Contact', text: bob.userName, }); - await alice1.navigateBack(); - await alice1.navigateBack(); + await alice1.navigateBack(false); + await alice1.navigateBack(false); await alice1.clickOnElementAll(new ExitUserProfile(alice1)); // Send message from Blocked User await bob1.sendMessage(blockedMessage); diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index a0d899a4..0c2e7a52 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -71,7 +71,7 @@ async function setNicknameAndroid(platform: SupportedPlatformsType) { }); const nickName = 'New nickname'; // Go back to conversation list - await alice1.navigateBack(true); + await alice1.navigateBack(); // Select conversation in list with Bob await alice1.longPressConversation(bob.userName); // Select 'Details' option diff --git a/run/test/specs/utils/create_group.ts b/run/test/specs/utils/create_group.ts index 9a2c1342..6b0271ec 100644 --- a/run/test/specs/utils/create_group.ts +++ b/run/test/specs/utils/create_group.ts @@ -27,14 +27,14 @@ export const createGroup = async ( const charlieMessage = `${userThree.userName} to ${userName}`; // Create contact between User A and User B await newContact(platform, device1, userOne, device2, userTwo); - await device1.navigateBack(true); + await device1.navigateBack(); await newContact(platform, device1, userOne, device3, userThree); - await device2.navigateBack(true); + await device2.navigateBack(); // Create contact between User A and User C // Exit conversation back to list - await device1.navigateBack(true); + await device1.navigateBack(); // Exit conversation back to list - await device3.navigateBack(true); + await device3.navigateBack(); // Click plus button await device1.clickOnElementAll(new PlusButton(device1)); // Select Closed Group option diff --git a/run/test/specs/utils/set_disappearing_messages.ts b/run/test/specs/utils/set_disappearing_messages.ts index 89f3cb95..e4d5dbd2 100644 --- a/run/test/specs/utils/set_disappearing_messages.ts +++ b/run/test/specs/utils/set_disappearing_messages.ts @@ -45,7 +45,7 @@ export const setDisappearingMessage = async ( await device.clickOnElementAll(new DisappearingMessageRadial(device, timerDuration)); await device.clickOnElementAll(new SetDisappearMessagesButton(device)); - await device.navigateBack(true); + await device.navigateBack(); // Extended the wait for the Follow settings button to settle in the UI, it was moving and confusing appium await sleepFor(2000); if (device2) { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 32723938..0fd4a53b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -267,6 +267,41 @@ export class DeviceWrapper { Array >; } + /** + * Attempts to click 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. + * + * @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 maxWait - Maximum wait time in milliseconds for each locator (default: 3000). + * @throws If neither locator is found. + */ + private async findWithFallback( + primaryLocator: LocatorsInterface | StrategyExtractionObj, + fallbackLocator: LocatorsInterface | StrategyExtractionObj, + maxWait: number = 3000 + ): Promise { + const primary = + 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( + `[navigateBack] 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; + } + throw new Error(`[navigateBack] Could not find primary or fallback locator`); + } public async longClick(element: AppiumNextElementType, durationMs: number) { if (this.isIOS()) { @@ -1761,27 +1796,49 @@ export class DeviceWrapper { } } - public async navigateBack(newAndroid?: boolean) { + public async navigateBack(newAndroid: boolean = true) { if (this.isIOS()) { await this.clickOnByAccessibilityID('Back'); - } else { - if (newAndroid) { - await this.clickOnElementAll({ strategy: 'id', selector: 'Navigate back' }); - } else { - await this.clickOnElementAll({ strategy: 'accessibility id', selector: 'Navigate up' }); - } + return; + } else if (this.isAndroid()) { + const newLocator = { + strategy: 'id', + selector: 'Navigate back', + } as StrategyExtractionObj; + const legacyLocator = { + strategy: 'accessibility id', + selector: 'Navigate up', + } as StrategyExtractionObj; + // Prefer new locator if newAndroid is true, otherwise prefer legacy + const [primary, fallback] = newAndroid + ? [newLocator, legacyLocator] + : [legacyLocator, newLocator]; + await this.findWithFallback(primary, fallback); } } - public async closeScreen(newAndroid?: boolean) { - if (this.isAndroid()) { - if (newAndroid) { - await this.clickOnElementAll({ strategy: 'id', selector: 'Close button' }); - } else { - await this.clickOnElementAll({ strategy: 'accessibility id', selector: 'Navigate up' }); - } - } else { + 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); } } @@ -1907,50 +1964,56 @@ export class DeviceWrapper { public async checkModalStrings( expectedHeading: string, expectedDescription: string, - oldModalAndroid?: boolean + newAndroid: boolean = true ) { - // Check modal heading is correct + const useNewLocator = this.isIOS() || newAndroid; + + // Sanitize function removeNewLines(input: string): string { return input.replace(/\n/gi, ''); } - let elHeading; - // Some modals in Android haven't been updated to compose yet therefore need different locators - if (!oldModalAndroid) { - elHeading = await this.waitForTextElementToBePresent(new ModalHeading(this)); - } else { - elHeading = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Modal heading', - }); - } - const actualHeading = await this.getTextFromElement(elHeading); + // Locators + 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; + + // Pick locator priority based on platform + const [headingPrimary, headingFallback] = useNewLocator + ? [newHeading, legacyHeading] + : [legacyHeading, newHeading]; + + const [descPrimary, descFallback] = useNewLocator + ? [newDescription, legacyDescription] + : [legacyDescription, newDescription]; + + // Modal Heading + const elHeading = await this.findWithFallback(headingPrimary, headingFallback); + const actualHeading = removeNewLines(await this.getTextFromElement(elHeading)); if (expectedHeading === actualHeading) { console.log('Modal heading is correct'); } else { throw new Error( - `Modal heading is incorrect. Expected heading: ${expectedHeading}, Actual heading: ${actualHeading}` + `Modal heading is incorrect.\nExpected: ${expectedHeading}\nActual: ${actualHeading}` ); } - // Now check modal description - let elDescription; - if (!oldModalAndroid) { - elDescription = await this.waitForTextElementToBePresent(new ModalDescription(this)); + // Modal Description + const elDescription = await this.findWithFallback(descPrimary, descFallback); + const actualDescription = removeNewLines(await this.getTextFromElement(elDescription)); + if (expectedDescription === actualDescription) { + console.log('Modal description is correct'); } else { - elDescription = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Modal description', - }); - } - const actualDescription = await this.getTextFromElement(elDescription); - // Need to format the ACTUAL description that comes back from device to match - const formattedDescription = removeNewLines(actualDescription); - if (expectedDescription !== formattedDescription) { throw new Error( - `Modal description is incorrect. Expected description: ${expectedDescription}, Actual description: ${formattedDescription}` + `Modal description is incorrect.\nExpected: ${expectedDescription}\nActual: ${actualDescription}` ); - } else { - console.log('Modal description is correct'); } } diff --git a/run/types/testing.ts b/run/types/testing.ts index 6e1cca1d..9394d4aa 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -464,10 +464,12 @@ export type Id = | 'Share button' | 'Call' | 'Conversation header name' + | 'block-user-menu-option' | 'block-user-confirm-button' | 'Notifications' | 'All Session notifications' - | 'com.android.settings:id/switch_text'; + | 'com.android.settings:id/switch_text' + | 'Block'; export type TestRisk = 'high' | 'medium' | 'low'; From 83bd036ca29d6e7ae9dc6dc4e7e83e7591850437 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Jun 2025 09:56:26 +1000 Subject: [PATCH 16/16] chore: bump port range again --- patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch | 4 ++-- yarn.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch b/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch index 276fa531..9122c396 100644 --- a/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch +++ b/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch @@ -1,5 +1,5 @@ diff --git a/build/lib/driver.js b/build/lib/driver.js -index 4287b4fc525093dee083bd37869947902bcc0398..dc1536388c2c9782c382509a9d6c0968d793b1b7 100644 +index 4287b4fc525093dee083bd37869947902bcc0398..8635fd3d871c55d7e54e22246f9b4b90fbd5686a 100644 --- a/build/lib/driver.js +++ b/build/lib/driver.js @@ -60,7 +60,7 @@ const screenshot_1 = require("./commands/screenshot"); @@ -7,7 +7,7 @@ index 4287b4fc525093dee083bd37869947902bcc0398..dc1536388c2c9782c382509a9d6c0968 // The range of ports we can use on the system for communicating to the // UiAutomator2 HTTP server on the device -const DEVICE_PORT_RANGE = [8200, 8299]; -+const DEVICE_PORT_RANGE = [8200, 8499]; ++const DEVICE_PORT_RANGE = [8200, 8999]; // The guard is needed to avoid dynamic system port allocation conflicts for // parallel driver sessions const DEVICE_PORT_ALLOCATION_GUARD = support_1.util.getLockFileGuard(node_path_1.default.resolve(node_os_1.default.tmpdir(), 'uia2_device_port_guard'), { timeout: 25, tryRecovery: true }); diff --git a/yarn.lock b/yarn.lock index 03b9539f..aeac979c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1686,7 +1686,7 @@ __metadata: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch": version: 3.8.2 - resolution: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch::version=3.8.2&hash=84490b" + resolution: "appium-uiautomator2-driver@patch:appium-uiautomator2-driver@npm%3A3.8.2#~/patches/appium-uiautomator2-driver-npm-3.8.2-1ce2a0f39e.patch::version=3.8.2&hash=4b0b2d" dependencies: appium-adb: "npm:^12.7.0" appium-android-driver: "npm:^9.12.3" @@ -1703,7 +1703,7 @@ __metadata: type-fest: "npm:^4.4.0" peerDependencies: appium: ^2.4.1 - checksum: 10c0/b1c7b9447a67acf9424a164bfa53329693e9b4d532c8b61c08325aab6793ef52633781c08f16d6e0aac0731c49526b76edd02ae9ce1eb756461ec1b51c9699e1 + checksum: 10c0/51fbe71a9054192e867f57126f4f09087713681bb97cf2ed47953460bda6f2451d560e45669ff5fcee34fbbb368f59f883899db57d972f9318a2daad8d996044 languageName: node linkType: hard