From 7a6cf5c507b928606c057e580f7fc99c74a5ba65 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 12 Aug 2025 14:28:53 +1000 Subject: [PATCH 01/88] fix: weed out all contact and conversation list item locators --- .../specs/group_tests_add_contact.spec.ts | 3 +- .../specs/linked_device_block_user.spec.ts | 13 ++---- .../linked_device_delete_message.spec.ts | 4 +- run/test/specs/locators/global.ts | 14 +++++- run/test/specs/locators/home.ts | 21 ++++++--- .../specs/message_requests_accept.spec.ts | 19 +++----- run/test/specs/message_requests_block.spec.ts | 18 +++----- ...er_actions_block_conversation_list.spec.ts | 15 +++---- ...actions_block_conversation_options.spec.ts | 7 +-- .../specs/user_actions_create_contact.spec.ts | 43 +++---------------- .../specs/user_actions_read_status.spec.ts | 13 ++---- .../user_actions_share_to_session.spec.ts | 8 ++-- .../utils/disappearing_control_messages.ts | 7 +-- run/types/DeviceWrapper.ts | 28 +++--------- run/types/testing.ts | 10 ++++- 15 files changed, 80 insertions(+), 143 deletions(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index ddf444e8f..1981a6331 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -68,8 +68,7 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes await unknown1.navigateBack(); // Leave Message Requests screen (Android) await unknown1.onAndroid().navigateBack(); - await unknown1.selectByText('Conversation list item', group.groupName); - // Check for control message on device 4 + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); await closeApp(alice1, bob1, charlie1, unknown1); } diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index c7ed79837..d288886ab 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -4,6 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal } from './locators'; import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { Contact } from './locators/global'; import { ConversationItem } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice2_Bob1_friends } from './state_builder'; @@ -73,16 +74,8 @@ async function blockUserInConversationOptions( alice2.clickOnElementAll(new BlockedContactsSettings(alice2)), ]); await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }), + alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)), + alice2.waitForTextElementToBePresent(new Contact(alice2, bob.userName)), ]); // Close app await closeApp(alice1, bob1, alice2); diff --git a/run/test/specs/linked_device_delete_message.spec.ts b/run/test/specs/linked_device_delete_message.spec.ts index bba534748..c4a0822a0 100644 --- a/run/test/specs/linked_device_delete_message.spec.ts +++ b/run/test/specs/linked_device_delete_message.spec.ts @@ -25,9 +25,7 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test 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(new ConversationItem(alice2, bob.userName)); - await alice2.selectByText('Conversation list item', bob.userName); - // Find message + await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); // Find message await alice2.findMessageWithBody(sentMessage); // Select message on device 1, long press await alice1.longPressMessage(sentMessage); diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index d45a5e650..94abe2b7f 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -1,3 +1,4 @@ +import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { LocatorsInterface } from './index'; export class ModalHeading extends LocatorsInterface { @@ -61,17 +62,26 @@ export class EnableLinkPreviewsModalButton extends LocatorsInterface { } export class Contact extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Contact', + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("pro-badge-text").text("${this.text}")`, + // strategy: 'id', + // selector: 'pro-badge-text', + // text: this.text } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Contact', + text: this.text, } as const; } } diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index a9e11fad0..b89e179a5 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -40,11 +40,22 @@ export class ConversationItem extends LocatorsInterface { this.text = text; } public build() { - return { - strategy: 'accessibility id', - selector: 'Conversation list item', - text: this.text, - } as const; + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("pro-badge-text").text("${this.text}")`, + // strategy: 'id', + // selector: 'pro-badge-text', + // text: this.text + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Conversation list item', + text: this.text, + } as const; + } } } diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index 4b64cafec..cdce72623 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppThreeDevices, SupportedPlatformsType } from './utils/open_app'; @@ -45,18 +45,11 @@ async function acceptRequest(platform: SupportedPlatformsType, testInfo: TestInf // Check conversation list for new contact (user A) await device2.navigateBack(); await device2.onAndroid().navigateBack(false); - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }), - ]); + await Promise.all( + [device2, device3].map(device => + device.waitForTextElementToBePresent(new ConversationItem(device, alice.userName)) + ) + ); // Close app await closeApp(device1, device2, device3); } diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index e7414deaa..463d650bc 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -4,6 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { BlockedContactsSettings } from './locators'; +import { Contact } from './locators/global'; import { MessageRequestsBanner, PlusButton } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; @@ -77,18 +78,11 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn device2.clickOnElementAll(new BlockedContactsSettings(device2)), device3.clickOnElementAll(new BlockedContactsSettings(device3)), ]); - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: alice.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: alice.userName, - }), - ]); + await Promise.all( + [device2, device3].map(device => + device.waitForTextElementToBePresent(new Contact(device, alice.userName)) + ) + ); // 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 dfba6a7e2..37af68ce9 100644 --- a/run/test/specs/user_actions_block_conversation_list.spec.ts +++ b/run/test/specs/user_actions_block_conversation_list.spec.ts @@ -4,7 +4,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BlockedContactsSettings } from './locators'; -import { LongPressBlockOption } from './locators/home'; +import { Contact } from './locators/global'; +import { ConversationItem, LongPressBlockOption } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -43,20 +44,14 @@ async function blockUserInConversationList(platform: SupportedPlatformsType, tes await alice1.clickOnByAccessibilityID('Block'); // Once you block the conversation disappears from the home screen await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: bob.userName, - maxWait: 5000, + ...new ConversationItem(alice1, bob.userName).build(), + maxWait: 5_000, }); await alice1.clickOnElementAll(new UserSettings(alice1)); // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it await alice1.scrollDown(); await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); + await alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/user_actions_block_conversation_options.spec.ts b/run/test/specs/user_actions_block_conversation_options.spec.ts index c8ef5175e..d7290a1e9 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -9,6 +9,7 @@ import { ExitUserProfile, } from './locators'; import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { Contact } from './locators/global'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -73,11 +74,7 @@ async function blockUserInConversationSettings( await alice1.clickOnElementAll(new ConversationsMenuItem(alice1)); await alice1.clickOnElementAll(new BlockedContactsSettings(alice1)); // Accessibility ID for Blocked Contact not present on iOS - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); + await alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)); await alice1.navigateBack(false); await alice1.navigateBack(false); await alice1.clickOnElementAll(new ExitUserProfile(alice1)); diff --git a/run/test/specs/user_actions_create_contact.spec.ts b/run/test/specs/user_actions_create_contact.spec.ts index 86869ac5c..3dd56e1d1 100644 --- a/run/test/specs/user_actions_create_contact.spec.ts +++ b/run/test/specs/user_actions_create_contact.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { retryMsgSentForBanner } from './utils/create_contact'; import { linkedDevice } from './utils/link_device'; @@ -48,42 +48,11 @@ async function createContact(platform: SupportedPlatformsType, testInfo: TestInf await device1.navigateBack(); await device2.navigateBack(); // Check username has changed from session id on both device 1 and 3 - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: Bob.userName, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: Bob.userName, - }), - ]); - // Check contact is added to contacts list on device 1 and 3 (linked device) - // await Promise.all([ - // device1.clickOnElementAll({ - // strategy: "accessibility id", - // selector: "New conversation button", - // }), - // device3.clickOnElementAll({ - // strategy: "accessibility id", - // selector: "New conversation button", - // }), - // ]); + await Promise.all( + [device1, device3].map(device => + device.waitForTextElementToBePresent(new ConversationItem(device, Bob.userName)) + ) + ); - // NEED CONTACT ACCESSIBILITY ID TO BE ADDED - // await Promise.all([ - // device1.waitForTextElementToBePresent({ - // strategy: "accessibility id", - // selector: "Contacts", - // }), - // device3.waitForTextElementToBePresent({ - // strategy: "accessibility id", - // selector: "Contacts", - // }), - // ]); - - // Wait for tick await closeApp(device1, device2, device3); } diff --git a/run/test/specs/user_actions_read_status.spec.ts b/run/test/specs/user_actions_read_status.spec.ts index 3a7676d1f..eb7d96545 100644 --- a/run/test/specs/user_actions_read_status.spec.ts +++ b/run/test/specs/user_actions_read_status.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils/index'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,19 +26,11 @@ async function readStatus(platform: SupportedPlatformsType, testInfo: TestInfo) // Go to settings to turn on read status // Device 1 await Promise.all([alice1.turnOnReadReceipts(), bob1.turnOnReadReceipts()]); - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: bob.userName, - }); + await alice1.clickOnElementAll(new ConversationItem(alice1, bob.userName)); // Send message from User A to User B to verify read status is working await alice1.sendMessage(testMessage); await sleepFor(100); - await bob1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: alice.userName, - }); + await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); await bob1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Message body', diff --git a/run/test/specs/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts index 2585a19c7..5af17119e 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -7,6 +7,7 @@ import { USERNAME } from '../../types/testing'; import { ImageName, ShareExtensionIcon } from './locators'; import { MessageInput, SendButton } from './locators/conversation'; import { PhotoLibrary } from './locators/external'; +import { Contact } from './locators/global'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { handlePhotosFirstTimeOpen } from './utils/handle_first_open'; @@ -27,6 +28,7 @@ bothPlatformsIt({ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { bob }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return open_Alice1_Bob1_friends({ platform, @@ -58,11 +60,7 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn await alice1.onAndroid().clickOnElementAll(new ImageName(alice1)); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Share' }); await alice1.clickOnElementAll(new ShareExtensionIcon(alice1)); - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Contact', - text: USERNAME.BOB, - }); + await alice1.clickOnElementAll(new Contact(alice1, bob.userName)); await alice1.inputText(testMessage, new MessageInput(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); // Loading screen... diff --git a/run/test/specs/utils/disappearing_control_messages.ts b/run/test/specs/utils/disappearing_control_messages.ts index 28e796b38..ea2b22a4b 100644 --- a/run/test/specs/utils/disappearing_control_messages.ts +++ b/run/test/specs/utils/disappearing_control_messages.ts @@ -3,6 +3,7 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { DisappearActions, DISAPPEARING_TIMES } from '../../../types/testing'; +import { ConversationItem } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; export const checkDisappearingControlMessage = async ( @@ -47,11 +48,7 @@ export const checkDisappearingControlMessage = async ( } // Check if control messages are syncing from both user A and user B if (linkedDevice) { - await linkedDevice.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: userNameB, - }); + await linkedDevice.clickOnElementAll(new ConversationItem(linkedDevice, userNameB)); await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetYou); await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetBob); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 1320bfed0..cc9026ae7 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -33,8 +33,8 @@ import { ScrollToBottomButton, SendButton, } from '../test/specs/locators/conversation'; -import { ModalDescription, ModalHeading } from '../test/specs/locators/global'; -import { PlusButton } from '../test/specs/locators/home'; +import { Contact, ModalDescription, ModalHeading } from '../test/specs/locators/global'; +import { ConversationItem, PlusButton } from '../test/specs/locators/home'; import { LoadingAnimation } from '../test/specs/locators/onboarding'; import { PrivacyMenuItem, @@ -516,11 +516,7 @@ export class DeviceWrapper { while (attempt < maxRetries && !success) { try { - const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: userName, - }); + const el = await this.waitForTextElementToBePresent(new ConversationItem(this, userName)); if (!el) { throw new Error( @@ -1470,17 +1466,7 @@ export class DeviceWrapper { public async sendMessageTo(sender: User, receiver: Group | User) { const message = `${sender.userName} to ${receiver.userName}`; - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: receiver.userName, - }); - await sleepFor(100); - await this.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Conversation list item', - text: receiver.userName, - }); + await this.clickOnElementAll(new ConversationItem(this, receiver.userName)); this.log(`${sender.userName} + " sent message to ${receiver.userName}`); await this.sendMessage(message); this.log(`Message received by ${receiver.userName} from ${sender.userName}`); @@ -1941,11 +1927,7 @@ export class DeviceWrapper { text: contact.userName, }); } else { - await this.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Contact', - text: contact.userName, - }); + await this.clickOnElementAll(new Contact(this, contact.userName)); } await this.clickOnElementAll(new SendButton(this)); await this.waitForTextElementToBePresent(new OutgoingMessageStatusSent(this)); diff --git a/run/types/testing.ts b/run/types/testing.ts index 180381ced..3f0a0e58a 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -57,7 +57,7 @@ export const InteractionPoints: Record = { BackToSession: { x: 42, y: 42 }, }; -export type Strategy = 'accessibility id' | 'class name' | 'id' | 'xpath'; +export type Strategy = '-android uiautomator' | 'accessibility id' | 'class name' | 'id' | 'xpath'; export type ConversationType = '1:1' | 'Community' | 'Group' | 'Note to Self'; @@ -97,6 +97,11 @@ export type DisappearOptsGroup = [ export type MergedOptions = DisappearOpts1o1 | DisappearOptsGroup; export type StrategyExtractionObj = + | { + strategy: Extract; + selector: UiAutomatorQuery; + text?: string; + } | { strategy: Extract; selector: AccessibilityId; @@ -148,6 +153,8 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/androidx.appcompat.widget.LinearLayoutCompat/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView[2]` | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; +export type UiAutomatorQuery = `new UiSelector().resourceId("pro-badge-text").text("${string}")`; + export type AccessibilityId = | DISAPPEARING_TIMES | UserNameType @@ -501,6 +508,7 @@ export type Id = | 'preferred-display-name' | 'Privacy' | 'Privacy Policy' + | 'pro-badge-text' | 'Quit' | 'rate-app-button' | 'Recovery password container' From bd073f968d6c87255273f5cb34f90b8451ff094b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 12 Aug 2025 14:53:10 +1000 Subject: [PATCH 02/88] feat: begin using uiscrollable instead of blind scrolling --- run/test/specs/app_disguise_icons.spec.ts | 4 ++-- run/test/specs/app_disguise_set.spec.ts | 3 --- run/test/specs/locators/settings.ts | 10 ++++++---- run/types/testing.ts | 5 ++++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/run/test/specs/app_disguise_icons.spec.ts b/run/test/specs/app_disguise_icons.spec.ts index 5ce75ee21..6f065fc39 100644 --- a/run/test/specs/app_disguise_icons.spec.ts +++ b/run/test/specs/app_disguise_icons.spec.ts @@ -26,11 +26,11 @@ async function appDisguiseIcons(platform: SupportedPlatformsType, testInfo: Test await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); // Must scroll down to reveal the Appearance menu item - await device.scrollDown(); + await device.onIOS().scrollDown(); await device.clickOnElementAll(new AppearanceMenuItem(device)); await sleepFor(2000); // Must scroll down to reveal the app disguise option - await device.scrollDown(); + await device.onIOS().scrollDown(); await device.clickOnElementAll(new SelectAppIcon(device)); await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); await closeApp(device); diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index f01490f7f..19de258ab 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -35,11 +35,8 @@ async function appDisguiseSetIcon(platform: SupportedPlatformsType, testInfo: Te const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); - // Must scroll down to reveal the Appearance menu item - await device.scrollDown(); await device.clickOnElementAll(new AppearanceMenuItem(device)); await sleepFor(2000); - // Must scroll down to reveal the app disguise option await device.scrollDown(); await device.clickOnElementAll(new SelectAppIcon(device)); try { diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index e9d741856..d7bfb4cbd 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -182,8 +182,9 @@ export class AppearanceMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Appearance', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))', } as const; case 'ios': return { @@ -199,8 +200,9 @@ export class SelectAppIcon extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'network.loki.messenger.qa:id/system_settings_app_icon', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))', } as const; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index 3f0a0e58a..fc0d01b7d 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -153,7 +153,10 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/androidx.appcompat.widget.LinearLayoutCompat/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.TextView[2]` | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; -export type UiAutomatorQuery = `new UiSelector().resourceId("pro-badge-text").text("${string}")`; +export type UiAutomatorQuery = + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' + | `new UiSelector().resourceId("pro-badge-text").text("${string}")`; export type AccessibilityId = | DISAPPEARING_TIMES From 44968cf8168ad16a6115c53a1a86ec0534a74daf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 12 Aug 2025 16:45:53 +1000 Subject: [PATCH 03/88] fix: only use locator class for accountid --- run/test/specs/invite_a_friend_share.spec.ts | 5 ++-- run/test/specs/linked_device.spec.ts | 8 ++----- run/test/specs/locators/global.ts | 24 +++++++++++++++++++ run/test/specs/locators/start_conversation.ts | 17 +------------ ...ser_actions_hide_recovery_password.spec.ts | 7 ++---- run/test/specs/utils/create_account.ts | 5 ++-- run/test/specs/utils/get_account_id.ts | 11 --------- run/test/specs/utils/index.ts | 3 --- 8 files changed, 35 insertions(+), 45 deletions(-) diff --git a/run/test/specs/invite_a_friend_share.spec.ts b/run/test/specs/invite_a_friend_share.spec.ts index a40a0147d..88ae0d575 100644 --- a/run/test/specs/invite_a_friend_share.spec.ts +++ b/run/test/specs/invite_a_friend_share.spec.ts @@ -3,8 +3,9 @@ import type { TestInfo } from '@playwright/test'; import { IOS_XPATHS } from '../../constants'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { AccountIDDisplay } from './locators/global'; import { PlusButton } from './locators/home'; -import { AccountIDField, InviteAFriendOption, ShareButton } from './locators/start_conversation'; +import { InviteAFriendOption, ShareButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -25,7 +26,7 @@ async function inviteAFriend(platform: SupportedPlatformsType, testInfo: TestInf // Select Invite a Friend await device.clickOnElementAll(new InviteAFriendOption(device)); // Check for presence of Account ID field - await device.waitForTextElementToBePresent(new AccountIDField(device)); + await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); // Tap Share await device.clickOnElementAll(new ShareButton(device)); // defining the "Hey..." message element to retrieve the share message from diff --git a/run/test/specs/linked_device.spec.ts b/run/test/specs/linked_device.spec.ts index 5931d00b1..ba3c6cd2f 100644 --- a/run/test/specs/linked_device.spec.ts +++ b/run/test/specs/linked_device.spec.ts @@ -4,6 +4,7 @@ import { USERNAME } from '@session-foundation/qa-seeder'; import { bothPlatformsIt } from '../../types/sessionIt'; import { UsernameSettings } from './locators'; +import { AccountIDDisplay } from './locators/global'; import { UserSettings } from './locators/settings'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; @@ -33,11 +34,6 @@ async function linkDevice(platform: SupportedPlatformsType, testInfo: TestInfo) ...new UsernameSettings(alice2).build(), text: alice.userName, }); - await alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Account ID', - text: alice.accountID, - }); - + await alice2.waitForTextElementToBePresent(new AccountIDDisplay(alice2,alice.accountID)); await closeApp(alice1, alice2); } diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index 94abe2b7f..0047686db 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -120,3 +120,27 @@ export class DenyPermissionLocator extends LocatorsInterface { } } } + +export class AccountIDDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Account ID', + text: this.text, + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Account ID', + text: this.text, + } as const; + } + } +} \ No newline at end of file diff --git a/run/test/specs/locators/start_conversation.ts b/run/test/specs/locators/start_conversation.ts index 2c7b911cd..9fe2ab8b5 100644 --- a/run/test/specs/locators/start_conversation.ts +++ b/run/test/specs/locators/start_conversation.ts @@ -138,22 +138,7 @@ export class EnterAccountID extends LocatorsInterface { } // INVITE A FRIEND SECTION -export class AccountIDField extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Account ID', - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Account ID', - } as const; - } - } -} + export class ShareButton extends LocatorsInterface { public build() { diff --git a/run/test/specs/user_actions_hide_recovery_password.spec.ts b/run/test/specs/user_actions_hide_recovery_password.spec.ts index ad9f1ac34..cd6e1fd3c 100644 --- a/run/test/specs/user_actions_hide_recovery_password.spec.ts +++ b/run/test/specs/user_actions_hide_recovery_password.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ContinueButton } from './locators/global'; +import { AccountIDDisplay, ContinueButton } from './locators/global'; import { HideRecoveryPasswordButton, RecoveryPasswordMenuItem, @@ -49,10 +49,7 @@ async function hideRecoveryPassword(platform: SupportedPlatformsType, testInfo: }); // Should be taken back to Settings page after hiding recovery password await device1.onAndroid().scrollUp(); - await device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Account ID', - }); + await device1.waitForTextElementToBePresent(new AccountIDDisplay(device1)); // Check that linked device still has Recovery Password await device2.clickOnElementAll(new UserSettings(device2)); await device2.scrollDown(); diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index cfccd1237..96d7f64e8 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -2,7 +2,7 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; -import { ContinueButton } from '../locators/global'; +import { AccountIDDisplay, ContinueButton } from '../locators/global'; import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locators/onboarding'; import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; import { UserSettings } from '../locators/settings'; @@ -56,7 +56,8 @@ export async function newUser( // Exit Modal await device.navigateBack(false); await device.clickOnElementAll(new UserSettings(device)); - const accountID = await device.grabTextFromAccessibilityId('Account ID'); + const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)) + const accountID = await device.getTextFromElement(el); await device.closeScreen(false); return { userName, accountID, recoveryPhrase }; } diff --git a/run/test/specs/utils/get_account_id.ts b/run/test/specs/utils/get_account_id.ts index bd1e5a0c3..7df51f175 100644 --- a/run/test/specs/utils/get_account_id.ts +++ b/run/test/specs/utils/get_account_id.ts @@ -1,17 +1,6 @@ -import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; import { SupportedPlatformsType } from './open_app'; -export const saveSessionIdIos = async (device: DeviceWrapper) => { - const selector = await device.grabTextFromAccessibilityId('Session ID generated'); - return selector; -}; - -export const getAccountId = async (device: DeviceWrapper) => { - const AccountId = await device.grabTextFromAccessibilityId('Account ID'); - - return AccountId; -}; export function sortByPubkey(...users: Array) { return [...users] diff --git a/run/test/specs/utils/index.ts b/run/test/specs/utils/index.ts index cbc92334b..fce6925b0 100644 --- a/run/test/specs/utils/index.ts +++ b/run/test/specs/utils/index.ts @@ -1,12 +1,9 @@ import { clickOnCoordinates } from './click_by_coordinates'; -import { getAccountId, saveSessionIdIos } from './get_account_id'; import { runOnlyOnAndroid, runOnlyOnIOS } from './run_on'; import { sleepFor } from './sleep_for'; export { sleepFor, - saveSessionIdIos, - getAccountId, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates, From 29f74287c2b7858a913345c2e6ef8c33282772d0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 13 Aug 2025 11:56:18 +1000 Subject: [PATCH 04/88] fix: Android locator changes --- run/test/specs/check_avatar_color.spec.ts | 4 +- run/test/specs/linked_device.spec.ts | 9 +- .../linked_device_change_username.spec.ts | 98 ++----------------- .../specs/linked_device_create_group.spec.ts | 8 +- ...inked_device_profile_picture_syncs.spec.ts | 4 +- .../specs/linked_device_restore_group.spec.ts | 4 +- run/test/specs/locators/conversation.ts | 15 ++- run/test/specs/locators/global.ts | 10 +- run/test/specs/locators/groups.ts | 2 +- run/test/specs/locators/home.ts | 8 +- run/test/specs/locators/index.ts | 67 +++++++++---- run/test/specs/locators/settings.ts | 19 +++- run/test/specs/locators/start_conversation.ts | 1 - run/test/specs/ons_resolve.spec.ts | 2 +- ...ser_actions_change_profile_picture.spec.ts | 4 +- .../user_actions_change_username.spec.ts | 75 ++------------ run/test/specs/utils/create_account.ts | 2 +- run/test/specs/utils/get_account_id.ts | 1 - run/test/specs/utils/index.ts | 7 +- run/test/specs/utils/join_community.ts | 4 +- run/types/DeviceWrapper.ts | 8 +- run/types/testing.ts | 7 +- 22 files changed, 129 insertions(+), 230 deletions(-) diff --git a/run/test/specs/check_avatar_color.spec.ts b/run/test/specs/check_avatar_color.spec.ts index aa97d488c..80dbbb1ae 100644 --- a/run/test/specs/check_avatar_color.spec.ts +++ b/run/test/specs/check_avatar_color.spec.ts @@ -4,7 +4,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationSettings } from './locators/conversation'; import { ConversationItem } from './locators/home'; -import { UserSettings } from './locators/settings'; +import { UserAvatar, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { isSameColor } from './utils/check_colour'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -35,7 +35,7 @@ async function avatarColor(platform: SupportedPlatformsType, testInfo: TestInfo) }); await test.step(`Get Alice's avatar color on their device from the Settings screen avatar`, async () => { await alice1.clickOnElementAll(new UserSettings(alice1)); - alice1PixelColor = await alice1.getElementPixelColor(new UserSettings(alice1)); + alice1PixelColor = await alice1.getElementPixelColor(new UserAvatar(alice1)); }); await test.step(`Get Alice's avatar color on bob's device from the Conversation Settings avatar`, async () => { await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); diff --git a/run/test/specs/linked_device.spec.ts b/run/test/specs/linked_device.spec.ts index ba3c6cd2f..ac0cfbefc 100644 --- a/run/test/specs/linked_device.spec.ts +++ b/run/test/specs/linked_device.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UsernameSettings } from './locators'; +import { UsernameDisplay } from './locators'; import { AccountIDDisplay } from './locators/global'; import { UserSettings } from './locators/settings'; import { linkedDevice } from './utils/link_device'; @@ -30,10 +30,7 @@ async function linkDevice(platform: SupportedPlatformsType, testInfo: TestInfo) // Verify username and session ID match await alice2.clickOnElementAll(new UserSettings(alice2)); // Check username - await alice2.waitForTextElementToBePresent({ - ...new UsernameSettings(alice2).build(), - text: alice.userName, - }); - await alice2.waitForTextElementToBePresent(new AccountIDDisplay(alice2,alice.accountID)); + await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, alice.userName)); + await alice2.waitForTextElementToBePresent(new AccountIDDisplay(alice2, alice.accountID)); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_change_username.spec.ts b/run/test/specs/linked_device_change_username.spec.ts index eb8ee325b..3aec6ddcd 100644 --- a/run/test/specs/linked_device_change_username.spec.ts +++ b/run/test/specs/linked_device_change_username.spec.ts @@ -1,29 +1,22 @@ import type { TestInfo } from '@playwright/test'; -import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { TickButton, UsernameInput, UsernameSettings } from './locators'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { ClearInputButton, EditUsernameButton, UsernameDisplay, UsernameInput } from './locators'; import { SaveNameChangeButton, UserSettings } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Change username linked device', risk: 'medium', countOfDevicesNeeded: 2, - ios: { - testCb: changeUsernameLinkediOS, - }, - android: { - testCb: changeUsernameLinkedAndroid, - }, + testCb: changeUsernameLinked, }); -async function changeUsernameLinkediOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function changeUsernameLinked(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, alice2 }, - prebuilt: { alice }, } = await open_Alice2({ platform, testInfo }); const newUsername = 'Alice in chains'; @@ -33,87 +26,12 @@ async function changeUsernameLinkediOS(platform: SupportedPlatformsType, testInf alice2.clickOnElementAll(new UserSettings(alice2)), ]); // select username - await alice1.clickOnElementAll(new UsernameSettings(alice1)); - await alice1.checkModalStrings( - englishStrippedStr('displayNameSet').toString(), - englishStrippedStr('displayNameVisible').toString() - ); + await alice1.clickOnElementAll(new EditUsernameButton(alice1)); // type in new username await sleepFor(100); - await alice1.deleteText(new UsernameInput(alice1)); + await alice1.clickOnElementAll(new ClearInputButton(alice1)); await alice1.inputText(newUsername, new UsernameInput(alice1)); await alice1.clickOnElementAll(new SaveNameChangeButton(alice1)); - const username = await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }); - const changedUsername = await alice1.getTextFromElement(username); - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await alice1.closeScreen(); - await alice1.clickOnElementAll(new UserSettings(alice1)); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - text: newUsername, - }), - ]); - await closeApp(alice1, alice2); -} - -async function changeUsernameLinkedAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { - devices: { alice1, alice2 }, - prebuilt: { alice }, - } = await open_Alice2({ platform, testInfo }); - - const newUsername = 'Alice in chains'; - // click on settings/profile avatar - await Promise.all([ - alice1.clickOnElementAll(new UserSettings(alice1)), - alice2.clickOnElementAll(new UserSettings(alice2)), - ]); - // select username - await alice1.clickOnElementAll(new UsernameSettings(alice1)); - // type in new username - await sleepFor(100); - await alice1.deleteText(new UsernameInput(alice1)); - await alice1.inputText(newUsername, new UsernameInput(alice1)); - await alice1.clickOnElementAll(new TickButton(alice1)); - const usernameEl = await alice1.waitForTextElementToBePresent(new UsernameSettings(alice1)); - const changedUsername = await alice1.getTextFromElement(usernameEl); - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - // Get the initial linked username from alice2 - const username2 = await alice2.waitForTextElementToBePresent(new UsernameSettings(alice2)); - let currentLinkedUsername = await alice2.getTextFromElement(username2); - - let currentWait = 0; - const waitPerLoop = 500; - const maxWait = 50000; - - do { - await sleepFor(waitPerLoop); - // Close the screen and navigate back to the User Settings - await alice2.closeScreen(); - await alice2.clickOnElementAll(new UserSettings(alice2)); - currentWait += waitPerLoop; - const linkedUsernameEl = await alice2.waitForTextElementToBePresent( - new UsernameSettings(alice2) - ); - currentLinkedUsername = await alice2.getTextFromElement(linkedUsernameEl); - } while (currentLinkedUsername === alice.userName && currentWait < maxWait); - { - alice2.log('Username not changed yet'); - } + await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, newUsername)); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 88eb0ac8f..40899c26d 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -70,9 +70,7 @@ async function linkedGroupiOS(platform: SupportedPlatformsType, testInfo: TestIn // Wait 5 seconds for name to update await sleepFor(5000); // Check linked device for name change (conversation header name) - await device2.waitForTextElementToBePresent( - new ConversationHeaderName(device2).build(newGroupName) - ); + await device2.waitForTextElementToBePresent(new ConversationHeaderName(device2, newGroupName)); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), @@ -118,9 +116,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType, testInfo: Te // Config message is "Group name is now {group_name}" await device1.waitForControlMessageToBePresent(groupNameNew); // Check linked device for name change (conversation header name) - await device2.waitForTextElementToBePresent( - new ConversationHeaderName(device2).build(newGroupName) - ); + await device2.waitForTextElementToBePresent(new ConversationHeaderName(device2, newGroupName)); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), diff --git a/run/test/specs/linked_device_profile_picture_syncs.spec.ts b/run/test/specs/linked_device_profile_picture_syncs.spec.ts index f8f7b83fb..de71facfa 100644 --- a/run/test/specs/linked_device_profile_picture_syncs.spec.ts +++ b/run/test/specs/linked_device_profile_picture_syncs.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UserSettings } from './locators/settings'; +import { UserAvatar } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,7 +26,7 @@ async function avatarRestored(platform: SupportedPlatformsType, testInfo: TestIn } = await open_Alice2({ platform, testInfo }); await alice1.uploadProfilePicture(); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await alice2.waitForElementColorMatch(new UserSettings(alice2), expectedPixelHexColor, { + await alice2.waitForElementColorMatch(new UserAvatar(alice2), expectedPixelHexColor, { maxWait: 20_000, elementTimeout: 500, }); diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 59028e1de..fadea66f7 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -32,9 +32,7 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo // Check that group has loaded on linked device await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded - await device4.waitForTextElementToBePresent( - new ConversationHeaderName(device4).build(testGroupName) - ); + await device4.waitForTextElementToBePresent(new ConversationHeaderName(device4, testGroupName)); // Check all messages are present await Promise.all([ device4.waitForTextElementToBePresent({ diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index f24e1a9b3..d26fd2031 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -130,19 +130,24 @@ export class CallButton extends LocatorsInterface { } export class ConversationHeaderName extends LocatorsInterface { - public build(text?: string) { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Conversation header name', - text, + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))`, + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Conversation header name', - text, + text: this.text, } as const; } } diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index 0047686db..c5c658d95 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -71,11 +71,9 @@ export class Contact extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: `new UiSelector().resourceId("pro-badge-text").text("${this.text}")`, - // strategy: 'id', - // selector: 'pro-badge-text', - // text: this.text + strategy: 'id', + selector: 'pro-badge-text', + text: this.text, } as const; case 'ios': return { @@ -143,4 +141,4 @@ export class AccountIDDisplay extends LocatorsInterface { } as const; } } -} \ No newline at end of file +} diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 47e76a8c9..d9dd2d424 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -259,7 +259,7 @@ export class GroupMember extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Contact', + selector: 'pro-badge-text', text: `${username}`, } as const; case 'ios': diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index b89e179a5..1b26d734f 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -43,11 +43,9 @@ export class ConversationItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: `new UiSelector().resourceId("pro-badge-text").text("${this.text}")`, - // strategy: 'id', - // selector: 'pro-badge-text', - // text: this.text + strategy: 'id', + selector: 'pro-badge-text', + text: this.text, } as const; case 'ios': return { diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 860a106e5..fe1cf9251 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -39,17 +39,6 @@ export function describeLocator(locator: StrategyExtractionObj & { text?: string export abstract class LocatorsInterfaceScreenshot extends LocatorsInterface { abstract screenshotFileName(state?: ElementStates): string; } -// When applying a nickname or username change -export class TickButton extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { strategy: 'accessibility id', selector: 'Set' } as const; - case 'ios': - return { strategy: 'accessibility id', selector: 'Done' } as const; - } - } -} export class ApplyChanges extends LocatorsInterface { public build() { @@ -103,30 +92,54 @@ export class ExitUserProfile extends LocatorsInterface { } } -export class UsernameSettings extends LocatorsInterface { +export class UsernameDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Display name', + strategy: 'id', + selector: 'pro-badge-text', + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Username', + text: this.text, } as const; } } } -export class UsernameInput extends LocatorsInterface { +export class EditUsernameButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'accessibility id', - selector: 'Enter display name', + selector: 'Edit', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Username input', + } as const; + } + } +} + +export class UsernameInput extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'class name', + selector: 'android.widget.EditText', } as const; case 'ios': return { @@ -137,6 +150,23 @@ export class UsernameInput extends LocatorsInterface { } } +export class ClearInputButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'clear-input-button', + } as const; + case 'ios': + return { + strategy: 'id', + selector: 'clear-input-button', + } as const; + } + } +} + export class FirstGif extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -359,8 +389,9 @@ export class BlockedContactsSettings extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Blocked contacts', + // Temporary fix until there's a unique ID + strategy: '-android uiautomator', + selector: `new UiSelector().text("View and manage blocked contacts.")`, }; case 'ios': return { diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index d7bfb4cbd..7216eadf2 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -44,6 +44,23 @@ export class UserSettings extends LocatorsInterface { } } +export class UserAvatar extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'User settings', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'User settings', + } as const; + } + } +} + export class RecoveryPasswordMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -117,7 +134,7 @@ export class SaveNameChangeButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Save', + selector: 'update-username-confirm-button', } as const; case 'ios': return { diff --git a/run/test/specs/locators/start_conversation.ts b/run/test/specs/locators/start_conversation.ts index 9fe2ab8b5..92042db94 100644 --- a/run/test/specs/locators/start_conversation.ts +++ b/run/test/specs/locators/start_conversation.ts @@ -139,7 +139,6 @@ export class EnterAccountID extends LocatorsInterface { // INVITE A FRIEND SECTION - export class ShareButton extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/ons_resolve.spec.ts b/run/test/specs/ons_resolve.spec.ts index d9d270c57..b22edcb2b 100644 --- a/run/test/specs/ons_resolve.spec.ts +++ b/run/test/specs/ons_resolve.spec.ts @@ -42,7 +42,7 @@ async function resolveONS(platform: SupportedPlatformsType, testInfo: TestInfo) }); await test.step(`Verify ONS resolution to pubkey '${expectedPubkey}'`, async () => { await device.waitForTextElementToBePresent({ - ...new ConversationHeaderName(device).build(expectedPubkey), + ...new ConversationHeaderName(device, expectedPubkey).build(), maxWait: 5_000, }); }); diff --git a/run/test/specs/user_actions_change_profile_picture.spec.ts b/run/test/specs/user_actions_change_profile_picture.spec.ts index 9aa6b4efa..63cdc084f 100644 --- a/run/test/specs/user_actions_change_profile_picture.spec.ts +++ b/run/test/specs/user_actions_change_profile_picture.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { UserSettings } from './locators/settings'; +import { UserAvatar } from './locators/settings'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -31,7 +31,7 @@ async function changeProfilePicture(platform: SupportedPlatformsType, testInfo: await device.uploadProfilePicture(); }); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await device.waitForElementColorMatch(new UserSettings(device), expectedPixelHexColor, { + await device.waitForElementColorMatch(new UserAvatar(device), expectedPixelHexColor, { maxWait: 10_000, elementTimeout: 500, }); diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index 006316fc1..73fad9c1b 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -1,92 +1,35 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { TickButton, UsernameInput, UsernameSettings } from './locators'; +import { ClearInputButton, EditUsernameButton, UsernameDisplay, UsernameInput } from './locators'; import { SaveNameChangeButton, UserSettings } from './locators/settings'; -import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Change username', risk: 'medium', countOfDevicesNeeded: 1, - ios: { - testCb: changeUsernameiOS, - }, - android: { - testCb: changeUsernameAndroid, - }, + testCb: changeUsername, }); -async function changeUsernameiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function changeUsername(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - const alice = await newUser(device, USERNAME.ALICE); + await newUser(device, USERNAME.ALICE); const newUsername = 'Alice in chains'; // click on settings/profile avatar await device.clickOnElementAll(new UserSettings(device)); // select username - await device.clickOnElementAll(new UsernameSettings(device)); - // New modal pops up + await device.clickOnElementAll(new EditUsernameButton(device)); await device.checkModalStrings( englishStrippedStr('displayNameSet').toString(), englishStrippedStr('displayNameVisible').toString() ); - // type in new username - await sleepFor(100); - await device.deleteText(new UsernameInput(device)); + await device.clickOnElementAll(new ClearInputButton(device)); await device.inputText(newUsername, new UsernameInput(device)); await device.clickOnElementAll(new SaveNameChangeButton(device)); - const username = await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Username', - }); - const changedUsername = await device.getTextFromElement(username); - device.log('Changed username', changedUsername); - if (changedUsername === newUsername) { - device.log('Username change successful'); - } - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await device.closeScreen(); - await closeApp(device); -} - -async function changeUsernameAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - const alice = await newUser(device, USERNAME.ALICE); - const newUsername = 'Alice in chains'; - // click on settings/profile avatar - await device.clickOnElementAll(new UserSettings(device)); - // select username - await device.clickOnElementAll(new UsernameSettings(device)); - // type in new username - await sleepFor(100); - await device.deleteText(new UsernameInput(device)); - await device.inputText(newUsername, new UsernameInput(device)); - await device.clickOnElementAll(new TickButton(device)); - const username = await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Display name', - text: newUsername, - }); - const changedUsername = await device.getTextFromElement(username); - device.log('Changed username', changedUsername); - if (changedUsername === newUsername) { - device.log('Username change successful'); - } - if (changedUsername === alice.userName) { - throw new Error('Username change unsuccessful'); - } - await device.closeScreen(); - await device.clickOnElementAll(new UserSettings(device)); - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Display name', - text: newUsername, - }); + await device.waitForTextElementToBePresent(new UsernameDisplay(device, newUsername)); await closeApp(device); } diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 96d7f64e8..50ef4dc3c 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -56,7 +56,7 @@ export async function newUser( // Exit Modal await device.navigateBack(false); await device.clickOnElementAll(new UserSettings(device)); - const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)) + const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); await device.closeScreen(false); return { userName, accountID, recoveryPhrase }; diff --git a/run/test/specs/utils/get_account_id.ts b/run/test/specs/utils/get_account_id.ts index 7df51f175..4820217e2 100644 --- a/run/test/specs/utils/get_account_id.ts +++ b/run/test/specs/utils/get_account_id.ts @@ -1,7 +1,6 @@ import { User } from '../../../types/testing'; import { SupportedPlatformsType } from './open_app'; - export function sortByPubkey(...users: Array) { return [...users] .sort((a, b) => a.accountID.localeCompare(b.accountID)) diff --git a/run/test/specs/utils/index.ts b/run/test/specs/utils/index.ts index fce6925b0..66e353af7 100644 --- a/run/test/specs/utils/index.ts +++ b/run/test/specs/utils/index.ts @@ -2,9 +2,4 @@ import { clickOnCoordinates } from './click_by_coordinates'; import { runOnlyOnAndroid, runOnlyOnIOS } from './run_on'; import { sleepFor } from './sleep_for'; -export { - sleepFor, - runOnlyOnIOS, - runOnlyOnAndroid, - clickOnCoordinates, -}; +export { sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; diff --git a/run/test/specs/utils/join_community.ts b/run/test/specs/utils/join_community.ts index 1b845d6b4..a18102732 100644 --- a/run/test/specs/utils/join_community.ts +++ b/run/test/specs/utils/join_community.ts @@ -13,7 +13,5 @@ export const joinCommunity = async ( await device.clickOnElementAll(new JoinCommunityOption(device)); await device.inputText(communityLink, new CommunityInput(device)); await device.clickOnElementAll(new JoinCommunityButton(device)); - await device.waitForTextElementToBePresent( - new ConversationHeaderName(device).build(communityName) - ); + await device.waitForTextElementToBePresent(new ConversationHeaderName(device, communityName)); }; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index cc9026ae7..1ef316946 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -39,6 +39,7 @@ import { LoadingAnimation } from '../test/specs/locators/onboarding'; import { PrivacyMenuItem, SaveProfilePictureButton, + UserAvatar, UserSettings, } from '../test/specs/locators/settings'; import { @@ -1859,7 +1860,7 @@ export class DeviceWrapper { public async uploadProfilePicture() { await this.clickOnElementAll(new UserSettings(this)); // Click on Profile picture - await this.clickOnElementAll(new UserSettings(this)); + await this.clickOnElementAll(new UserAvatar(this)); await this.clickOnElementAll(new ChangeProfilePictureButton(this)); if (this.isIOS()) { // Push file first @@ -2025,15 +2026,16 @@ export class DeviceWrapper { await this.scroll({ x: width / 2, y: height * 0.95 }, { x: width / 2, y: height * 0.35 }, 100); } + public async scrollToBottom() { try { const scrollButton = await this.waitForTextElementToBePresent({ ...new ScrollToBottomButton(this).build(), - maxWait: 1_000, + maxWait: 3_000, }); await this.click(scrollButton.ELEMENT); } catch { - this.info('Scroll button not found after 1s, continuing'); + this.info('Scroll button not found, continuing'); } } diff --git a/run/types/testing.ts b/run/types/testing.ts index fc0d01b7d..406815c3b 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -156,7 +156,9 @@ export type XPath = export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' - | `new UiSelector().resourceId("pro-badge-text").text("${string}")`; + | 'new UiSelector().text("Enter your display name")' + | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` + | `new UiSelector().text(${string})`; export type AccessibilityId = | DISAPPEARING_TIMES @@ -408,6 +410,7 @@ export type Id = | 'Call' | 'clear-input-button-description' | 'clear-input-button-name' + | 'clear-input-button' | 'Close button' | 'com.android.chrome:id/negative_button' | 'com.android.chrome:id/signin_fre_dismiss_button' @@ -535,6 +538,8 @@ export type Id = | 'update-group-info-confirm-button' | 'update-group-info-description-input' | 'update-group-info-name-input' + | 'update-username-confirm-button' + | 'User settings' | 'Version warning banner' | 'Yes' | `All ${AppName} notifications` From c09ee8a4284431823ccc145258cd6dc2aaf9331e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 13 Aug 2025 15:57:39 +1000 Subject: [PATCH 05/88] fix: more Android locator changes --- ...inked_device_profile_picture_syncs.spec.ts | 10 +- run/test/specs/locators/conversation.ts | 4 +- run/test/specs/locators/global_search.ts | 2 +- run/test/specs/locators/groups.ts | 4 +- run/test/specs/locators/index.ts | 4 +- ...actions_block_conversation_options.spec.ts | 4 +- ...ser_actions_change_profile_picture.spec.ts | 8 +- run/test/specs/utils/create_account.ts | 3 +- run/test/specs/voice_calls.spec.ts | 5 +- run/types/DeviceWrapper.ts | 161 +++++++----------- run/types/testing.ts | 2 +- 11 files changed, 84 insertions(+), 123 deletions(-) diff --git a/run/test/specs/linked_device_profile_picture_syncs.spec.ts b/run/test/specs/linked_device_profile_picture_syncs.spec.ts index de71facfa..abd561abb 100644 --- a/run/test/specs/linked_device_profile_picture_syncs.spec.ts +++ b/run/test/specs/linked_device_profile_picture_syncs.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UserAvatar } from './locators/settings'; +import { UserSettings } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,10 +26,10 @@ async function avatarRestored(platform: SupportedPlatformsType, testInfo: TestIn } = await open_Alice2({ platform, testInfo }); await alice1.uploadProfilePicture(); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await alice2.waitForElementColorMatch(new UserAvatar(alice2), expectedPixelHexColor, { - maxWait: 20_000, - elementTimeout: 500, - }); + await alice2.waitForElementColorMatch( + { ...new UserSettings(alice2).build(), maxWait: 20_000 }, + expectedPixelHexColor + ); }); await closeApp(alice1, alice2); } diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index d26fd2031..d3a67f5ed 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -368,8 +368,8 @@ export class EditNicknameButton extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'edit-profile-icon', + strategy: 'accessibility id', + selector: 'Edit', } as const; case 'ios': return { diff --git a/run/test/specs/locators/global_search.ts b/run/test/specs/locators/global_search.ts index b08aab9f4..3230131fc 100644 --- a/run/test/specs/locators/global_search.ts +++ b/run/test/specs/locators/global_search.ts @@ -7,7 +7,7 @@ export class NoteToSelfOption extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger.qa:id/search_result_title', + selector: 'pro-badge-text', text: 'Note to Self', }; case 'ios': diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index d9dd2d424..f994e7e9f 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -70,8 +70,8 @@ export class UpdateGroupInformation extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'group-name', + strategy: 'accessibility id', + selector: 'Edit', }; case 'ios': { const groupName = this.groupName; diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index fe1cf9251..85cad5716 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -75,13 +75,13 @@ export class ReadReceiptsButton extends LocatorsInterface { } } -export class ExitUserProfile extends LocatorsInterface { +export class CloseSettings extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'accessibility id', - selector: 'Navigate up', + selector: 'Close', } as const; case 'ios': return { 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 d7290a1e9..effb6dde6 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 { BlockedContactsSettings, BlockUser, BlockUserConfirmationModal, - ExitUserProfile, + CloseSettings, } from './locators'; import { BlockedBanner, ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; @@ -77,7 +77,7 @@ async function blockUserInConversationSettings( await alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)); await alice1.navigateBack(false); await alice1.navigateBack(false); - await alice1.clickOnElementAll(new ExitUserProfile(alice1)); + await alice1.clickOnElementAll(new CloseSettings(alice1)); // Send message from Blocked User await bob1.sendMessage(blockedMessage); await alice1.verifyElementNotPresent({ diff --git a/run/test/specs/user_actions_change_profile_picture.spec.ts b/run/test/specs/user_actions_change_profile_picture.spec.ts index 63cdc084f..bdca8f7fd 100644 --- a/run/test/specs/user_actions_change_profile_picture.spec.ts +++ b/run/test/specs/user_actions_change_profile_picture.spec.ts @@ -31,10 +31,10 @@ async function changeProfilePicture(platform: SupportedPlatformsType, testInfo: await device.uploadProfilePicture(); }); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { - await device.waitForElementColorMatch(new UserAvatar(device), expectedPixelHexColor, { - maxWait: 10_000, - elementTimeout: 500, - }); + await device.waitForElementColorMatch( + { ...new UserAvatar(device).build(), maxWait: 10_000 }, + expectedPixelHexColor + ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 50ef4dc3c..3c5eb2d7d 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -2,6 +2,7 @@ import type { UserNameType } from '@session-foundation/qa-seeder'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; +import { CloseSettings } from '../locators'; import { AccountIDDisplay, ContinueButton } from '../locators/global'; import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locators/onboarding'; import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; @@ -58,6 +59,6 @@ export async function newUser( await device.clickOnElementAll(new UserSettings(device)); const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); - await device.closeScreen(false); + await device.waitForTextElementToBePresent(new CloseSettings(device)); return { userName, accountID, recoveryPhrase }; } diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 0aea8678b..cca058d41 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -3,6 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { CloseSettings } from './locators'; import { CallButton, NotificationSettings, NotificationSwitch } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils/index'; @@ -77,7 +78,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo ); } }); - await alice1.closeScreen(); + await alice1.clickOnElementAll(new CloseSettings(alice1)); // Alice tries again, call is created but Bob still hasn't enabled their calls perms so this will fail await test.step(TestSteps.CALLS.INITIATE_CALL(alice.userName), async () => { await alice1.clickOnElementAll(new CallButton(alice1)); @@ -118,7 +119,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo Retrying won't help - use a real device where you can manually enable the permission.` ); } - await bob1.closeScreen(); + await bob1.clickOnElementAll(new CloseSettings(bob1)); await alice1.clickOnElementAll(new CallButton(alice1)); await bob1.clickOnByAccessibilityID('Answer call'); await Promise.all( diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 1ef316946..32d85a3fc 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -10,6 +10,7 @@ import * as sinon from 'sinon'; import { ChangeProfilePictureButton, + CloseSettings, describeLocator, DownloadMediaButton, FirstGif, @@ -311,15 +312,18 @@ export class DeviceWrapper { Array >; } + /** - * Attempts to click an element using a primary locator, and if not found, falls back to a secondary locator. + * Attempts to find an element using a primary locator, and if not found, falls back to a secondary locator. * This is useful for supporting UI transitions (e.g., between legacy and Compose Android screens) where - * the same UI element may have different locators depending context. + * the same UI element may have different locators depending on context. * - * @param primaryLocator - The first locator to try (e.g., new Compose locator or legacy locator). - * @param fallbackLocator - The locator to try if the primary is not found. + * @param primaryLocator - The first locator to try (e.g., new Compose locator). + * @param fallbackLocator - The locator to try if the primary is not found (e.g., legacy locator). * @param maxWait - Maximum wait time in milliseconds for each locator (default: 3000). - * @throws If neither locator is found. + * @returns The found element, which can be used for clicking, text extraction, or other operations. + * @throws If neither locator finds an element within the timeout period. + * */ public async findWithFallback( primaryLocator: LocatorsInterface | StrategyExtractionObj, @@ -330,21 +334,23 @@ export class DeviceWrapper { primaryLocator instanceof LocatorsInterface ? primaryLocator.build() : primaryLocator; const fallback = fallbackLocator instanceof LocatorsInterface ? fallbackLocator.build() : fallbackLocator; - let found = await this.doesElementExist({ ...primary, maxWait }); - if (found) { - await this.clickOnElementAll(primary); - return found; - } - console.warn( - `[findWithFallback] Could not find primary locator with '${primary.strategy}', falling back on '${fallback.strategy}'` - ); - found = await this.doesElementExist({ ...fallback, maxWait }); - if (found) { - await this.clickOnElementAll(fallback); - return found; + const primaryDescription = describeLocator(primary); + const fallbackDescription = describeLocator(fallback); + + try { + return await this.waitForTextElementToBePresent({ ...primary, maxWait }); + } catch (primaryError) { + console.warn( + `[findWithFallback] Could not find element with ${primaryDescription}, falling back to ${fallbackDescription}` + ); + + try { + return await this.waitForTextElementToBePresent({ ...fallback, maxWait }); + } catch (fallbackError) { + throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`); + } } - throw new Error(`[findWithFallback] Could not find primary or fallback locator`); } public async longClick(element: AppiumNextElementType, durationMs: number) { @@ -1324,66 +1330,36 @@ export class DeviceWrapper { this.error(`${lastError} after ${attempt} attempts (${elapsed}ms)`); throw new Error(lastError || 'Polling failed'); } - - /** - * Wait for an element to meet a specific condition - */ - async waitForElementCondition( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), - checkElement: (element: AppiumNextElementType) => Promise>, - options: { - maxWait?: number; - elementTimeout?: number; - } = {} - ): Promise { - const { elementTimeout = 500 } = options; - return this.pollUntil(async () => { - try { - // Convert to StrategyExtractionObj if needed - const locator = args instanceof LocatorsInterface ? args.build() : args; - - // Create new args with short timeout for polling - const pollArgs = { - ...locator, - text: args.text, - maxWait: elementTimeout, // Short timeout for each poll attempt - }; - - const element = await this.waitForTextElementToBePresent(pollArgs); - return await checkElement(element); - } catch (error) { - return { - success: false, - error: `Element not found: ${error instanceof Error ? error.message : String(error)}`, - }; - } - }, options); - } /** * Waits for an element's screenshot to match a specific color. * - * @param args - Element locator + * @param args - Element locator with optional text and maxWait * @param expectedColor - Hex color code (e.g., '04cbfe') - * @param options - Optional timeouts: maxWait (total) and elementTimeout (per check) * @throws If color doesn't match within timeout - * */ + public async waitForElementColorMatch( args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), - expectedColor: string, - options: { - maxWait?: number; - elementTimeout?: number; - } = {} + expectedColor: string ): Promise { - await this.waitForElementCondition( - args, - async (element): Promise => { - // Capture screenshot of the element as base64 + const locator = args instanceof LocatorsInterface ? args.build() : args; + const description = describeLocator({ ...locator, text: args.text }); + + this.log(`Waiting for ${description} to have color #${expectedColor}`); + + await this.pollUntil( + async () => { + const element = await this.findElementQuietly(locator, args.text); + + if (!element) { + return { + success: false, + error: `Element not found`, + }; + } + const base64 = await this.getElementScreenshot(element.ELEMENT); - // Extract the middle pixel color from the screenshot const actualColor = await parseDataImage(base64); - // Compare colors using the standard color matcher const matches = isSameColor(expectedColor, actualColor); return { @@ -1393,10 +1369,11 @@ export class DeviceWrapper { : `Color mismatch: expected #${expectedColor}, got #${actualColor}`, }; }, - options + { + maxWait: args.maxWait, // Will use default from pollUntil if undefined + } ); } - // UTILITY FUNCTIONS public async sendMessage(message: string) { @@ -2072,32 +2049,8 @@ export class DeviceWrapper { const [primary, fallback] = newAndroid ? [newLocator, legacyLocator] : [legacyLocator, newLocator]; - await this.findWithFallback(primary, fallback); - } - } - - public async closeScreen(newAndroid: boolean = true) { - if (this.isIOS()) { - await this.clickOnByAccessibilityID('Close button'); - return; - } - - if (this.isAndroid()) { - const newLocator = { - strategy: 'id', - selector: 'Close button', - } as StrategyExtractionObj; - - const legacyLocator = { - strategy: 'accessibility id', - selector: 'Navigate up', - } as StrategyExtractionObj; - - const [primary, fallback] = newAndroid - ? [newLocator, legacyLocator] - : [legacyLocator, newLocator]; - - await this.findWithFallback(primary, fallback); + const el = await this.findWithFallback(primary, fallback); + await this.click(el.ELEMENT); } } @@ -2121,7 +2074,7 @@ export class DeviceWrapper { await this.clickOnElementAll(new ReadReceiptsButton(this)); await this.navigateBack(false); await sleepFor(100); - await this.closeScreen(false); + await this.clickOnElementAll(new CloseSettings(this)); } public async processPermissions(locator: LocatorsInterface) { @@ -2290,13 +2243,19 @@ export class DeviceWrapper { } await this.clickOnElementAll(new UserSettings(this)); - await this.scrollDown(); - const versionElement = await this.findElement( - 'id', - 'network.loki.messenger.qa:id/versionTextView' - ); + // Find the element using UiScrollable + const versionElement = await this.waitForTextElementToBePresent({ + strategy: '-android uiautomator', + selector: 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', + }); + + // Get the full text from the element const versionText = await this.getAttribute('text', versionElement.ELEMENT); + // versionText will be something like "Version 1.27.0 (4175 - ac77d8) - Mainnet" + + // Extract just the version number const match = versionText?.match(/(\d+\.\d+\.\d+)/); + // match[1] will be "1.27.0" if (!match) { throw new Error(`Could not extract version from: ${versionText}`); diff --git a/run/types/testing.ts b/run/types/testing.ts index 406815c3b..082d1bed5 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -156,6 +156,7 @@ export type XPath = export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().text("Enter your display name")' | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` | `new UiSelector().text(${string})`; @@ -451,7 +452,6 @@ export type Id = | 'Enter display name' | 'error-message' | 'group-description' - | 'group-name' | 'Group name' | 'Group name input' | 'hide-nts-confirm-button' From ba4db65c1a6643d6de801e0a6149b7a978a1c702 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 13 Aug 2025 17:46:38 +1000 Subject: [PATCH 06/88] fix: create account util clicks close settings --- run/test/specs/utils/create_account.ts | 2 +- run/types/DeviceWrapper.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 3c5eb2d7d..853ad31fa 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -59,6 +59,6 @@ export async function newUser( await device.clickOnElementAll(new UserSettings(device)); const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); - await device.waitForTextElementToBePresent(new CloseSettings(device)); + await device.clickOnElementAll(new CloseSettings(device)); return { userName, accountID, recoveryPhrase }; } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 32d85a3fc..014cac1b5 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2246,8 +2246,9 @@ export class DeviceWrapper { // Find the element using UiScrollable const versionElement = await this.waitForTextElementToBePresent({ strategy: '-android uiautomator', - selector: 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', - }); + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', + }); // Get the full text from the element const versionText = await this.getAttribute('text', versionElement.ELEMENT); From acb1bca54886f386d2d0484d71046ad91c6b54c6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 14 Aug 2025 10:56:42 +1000 Subject: [PATCH 07/88] fix: editusernamebutton is still username on ios --- run/test/specs/locators/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 85cad5716..06f101b63 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -127,7 +127,7 @@ export class EditUsernameButton extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Username input', + selector: 'Username', } as const; } } From f9bdc10829a2c4410353764c3ae52ed905509014 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 15 Aug 2025 10:23:03 +1000 Subject: [PATCH 08/88] fix: ios doesn't have clear input button --- run/test/specs/linked_device_change_username.spec.ts | 3 ++- run/test/specs/user_actions_change_username.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/run/test/specs/linked_device_change_username.spec.ts b/run/test/specs/linked_device_change_username.spec.ts index 3aec6ddcd..49e9312e5 100644 --- a/run/test/specs/linked_device_change_username.spec.ts +++ b/run/test/specs/linked_device_change_username.spec.ts @@ -29,7 +29,8 @@ async function changeUsernameLinked(platform: SupportedPlatformsType, testInfo: await alice1.clickOnElementAll(new EditUsernameButton(alice1)); // type in new username await sleepFor(100); - await alice1.clickOnElementAll(new ClearInputButton(alice1)); + await alice1.onIOS().deleteText(new UsernameInput(alice1)); + await alice1.onAndroid().clickOnElementAll(new ClearInputButton(alice1)); await alice1.inputText(newUsername, new UsernameInput(alice1)); await alice1.clickOnElementAll(new SaveNameChangeButton(alice1)); await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, newUsername)); diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index 73fad9c1b..1e08eddc5 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -27,7 +27,8 @@ async function changeUsername(platform: SupportedPlatformsType, testInfo: TestIn englishStrippedStr('displayNameSet').toString(), englishStrippedStr('displayNameVisible').toString() ); - await device.clickOnElementAll(new ClearInputButton(device)); + await device.onIOS().deleteText(new UsernameInput(device)); + await device.onAndroid().clickOnElementAll(new ClearInputButton(device)); await device.inputText(newUsername, new UsernameInput(device)); await device.clickOnElementAll(new SaveNameChangeButton(device)); await device.waitForTextElementToBePresent(new UsernameDisplay(device, newUsername)); From 64999058a8e880aadf54f29249c4c4b1d70896b9 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 15 Aug 2025 13:01:55 +1000 Subject: [PATCH 09/88] fix: don't console.error for not found --- 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 014cac1b5..915eb4957 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1327,7 +1327,7 @@ export class DeviceWrapper { } } while (elapsed < maxWait); // Log the error with details but only throw generic error so that they get grouped in the report - this.error(`${lastError} after ${attempt} attempts (${elapsed}ms)`); + this.log(`${lastError} after ${attempt} attempts (${elapsed}ms)`); throw new Error(lastError || 'Polling failed'); } /** From 311a9be80c2c21d51310fbbd88095ec80a73314b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 15 Aug 2025 13:37:50 +1000 Subject: [PATCH 10/88] feat: set app disguise test on iOS --- run/test/specs/app_disguise_set.spec.ts | 40 +++++++++++++++++++++---- run/test/specs/locators/settings.ts | 6 ++-- run/types/testing.ts | 2 ++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index 19de258ab..17edc40a7 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { DisguisedApp } from './locators/external'; import { @@ -18,12 +18,16 @@ import { openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/o import { closeApp } from './utils/open_app'; import { runScriptAndLog } from './utils/utilities'; -// iOS implementation blocked by SES-3809 -androidIt({ +bothPlatformsItSeparate({ title: 'App disguise set icon', risk: 'medium', countOfDevicesNeeded: 1, - testCb: appDisguiseSetIcon, + android: { + testCb: appDisguiseSetIconAndroid, + }, + ios: { + testCb: appDisguiseSetIconIOS, + }, allureSuites: { parent: 'Settings', suite: 'App Disguise', @@ -31,7 +35,33 @@ androidIt({ allureDescription: 'Verifies the alternate icon set on the App Disguise page is applied', }); -async function appDisguiseSetIcon(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function appDisguiseSetIconIOS(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + await sleepFor(2000); + await device.scrollDown(); + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await device.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'You have changed the icon for “Session”.' + }); + await device.clickOnElementAll({ + strategy: 'accessibility id', + selector: 'OK' + }); + // TODO maybe grab a screenshot of the disguised app and see what you can do with it + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await closeApp(device); + await runScriptAndLog(`xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, true); + } +} + +async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 7216eadf2..eb66ff200 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -254,8 +254,10 @@ export class AppDisguiseMeetingIcon extends LocatorsInterface { selector: 'MeetingSE option', } as const; case 'ios': - // NOTE see SES-3809 - throw new Error('No locators implemented for iOS'); + return { + strategy: 'accessibility id', + selector: 'Meetings option', + } as const; } } } diff --git a/run/types/testing.ts b/run/types/testing.ts index 082d1bed5..497e93198 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -295,6 +295,7 @@ export type AccessibilityId = | 'Manage Members' | 'Media message' | 'MeetingSE' + | 'Meetings option' | 'Mentions list' | 'Message body' | 'Message composition' @@ -391,6 +392,7 @@ export type AccessibilityId = | 'Voice message' | 'X' | 'Yes' + | 'You have changed the icon for “Session”.' | 'Your message request has been accepted.' | `${DISAPPEARING_TIMES} - Radio` | `${GROUPNAME}` From eb57eada641ec80be394229c3f8fe058c462d43f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 15 Aug 2025 13:40:01 +1000 Subject: [PATCH 11/88] fix: remove blind sleeps and scrolls --- run/test/specs/app_disguise_set.spec.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index 17edc40a7..f5d47cd1b 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -40,24 +40,25 @@ async function appDisguiseSetIconIOS(platform: SupportedPlatformsType, testInfo: await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new AppearanceMenuItem(device)); - await sleepFor(2000); - await device.scrollDown(); await device.clickOnElementAll(new SelectAppIcon(device)); try { await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'You have changed the icon for “Session”.' + strategy: 'accessibility id', + selector: 'You have changed the icon for “Session”.', }); await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'OK' + strategy: 'accessibility id', + selector: 'OK', }); - // TODO maybe grab a screenshot of the disguised app and see what you can do with it + // TODO maybe grab a screenshot of the disguised app and see what you can do with it } finally { // The disguised app must be uninstalled otherwise every following test will fail await closeApp(device); - await runScriptAndLog(`xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, true); + await runScriptAndLog( + `xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, + true + ); } } @@ -66,8 +67,6 @@ async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testI await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new AppearanceMenuItem(device)); - await sleepFor(2000); - await device.scrollDown(); await device.clickOnElementAll(new SelectAppIcon(device)); try { await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); From cea2cc719a05b0edf4607f9b0f987b0fb8b09685 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 18 Aug 2025 09:57:42 +1000 Subject: [PATCH 12/88] fix: standardise control message logging --- run/types/DeviceWrapper.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 915eb4957..253f788f4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -733,9 +733,6 @@ export class DeviceWrapper { const matching = await this.findAsync(elements, async e => { const text = await this.getTextFromElement(e); const isPartialMatch = text && text.toLowerCase().includes(textToLookFor.toLowerCase()); - if (isPartialMatch) { - this.info(`Text found to include ${textToLookFor}`); - } return Boolean(isPartialMatch); }); @@ -1225,6 +1222,7 @@ export class DeviceWrapper { text: string, maxWait = 15000 ): Promise { + this.log(`Waiting for control message "${text}" to be present`); const result = await this.pollUntil( async () => { try { @@ -1233,11 +1231,11 @@ export class DeviceWrapper { return element ? { success: true, data: element } - : { success: false, error: `Control message with text "${text}" not found` }; + : { success: false, error: `Control message "${text}" not found` }; } catch (err) { return { success: false, - error: err instanceof Error ? err.message : String(err), + error: `Control message "${text}" not found`, }; } }, @@ -1245,7 +1243,7 @@ export class DeviceWrapper { ); if (!result) { - throw new Error(`Control message "${text}" not found after ${maxWait}ms`); + throw new Error(`Waited too long for control message "${text}"`); } this.log(`Control message "${text}" has been found`); From d133b3af663dc13d1e7eb8482f623ecce6b725ee Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 18 Aug 2025 11:08:48 +1000 Subject: [PATCH 13/88] feat: truncate long selector descriptions --- run/test/specs/locators/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 06f101b63..9b438fc50 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -31,7 +31,16 @@ export abstract class LocatorsInterface { export function describeLocator(locator: StrategyExtractionObj & { text?: string }): string { const { strategy, selector, text } = locator; - const base = `${strategy} "${selector}"`; + + // Trim selector if its too long, show beginning and end + const maxSelectorLength = 80; + const halfLength = Math.floor(maxSelectorLength / 2); + const trimmedSelector = + selector.length > maxSelectorLength + ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` + : selector; + + const base = `${strategy} "${trimmedSelector}"`; return text ? `${base} and text "${text}"` : base; } From c73f623b83a3c5cd818b8b57badfaeec42007325 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 18 Aug 2025 11:35:08 +1000 Subject: [PATCH 14/88] feat: add steps to app disguise tests --- run/test/specs/app_disguise_icons.spec.ts | 32 +++--- run/test/specs/app_disguise_set.spec.ts | 123 +++++++++++++--------- run/types/allure.ts | 4 + 3 files changed, 96 insertions(+), 63 deletions(-) diff --git a/run/test/specs/app_disguise_icons.spec.ts b/run/test/specs/app_disguise_icons.spec.ts index 6f065fc39..312c9a5d2 100644 --- a/run/test/specs/app_disguise_icons.spec.ts +++ b/run/test/specs/app_disguise_icons.spec.ts @@ -1,9 +1,9 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { AppearanceMenuItem, SelectAppIcon, UserSettings } from './locators/settings'; -import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { AppDisguisePageScreenshot } from './utils/screenshot_paths'; @@ -22,16 +22,20 @@ bothPlatformsIt({ }); async function appDisguiseIcons(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - await device.clickOnElementAll(new UserSettings(device)); - // Must scroll down to reveal the Appearance menu item - await device.onIOS().scrollDown(); - await device.clickOnElementAll(new AppearanceMenuItem(device)); - await sleepFor(2000); - // Must scroll down to reveal the app disguise option - await device.onIOS().scrollDown(); - await device.clickOnElementAll(new SelectAppIcon(device)); - await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); - await closeApp(device); + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.VERIFY.ELEMENT_SCREENSHOT('app disguise icons'), async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); } diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index f5d47cd1b..15c7d3a84 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { DisguisedApp } from './locators/external'; @@ -36,55 +37,79 @@ bothPlatformsItSeparate({ }); async function appDisguiseSetIconIOS(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - await device.clickOnElementAll(new UserSettings(device)); - await device.clickOnElementAll(new AppearanceMenuItem(device)); - await device.clickOnElementAll(new SelectAppIcon(device)); - try { - await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'You have changed the icon for “Session”.', - }); - await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'OK', - }); - // TODO maybe grab a screenshot of the disguised app and see what you can do with it - } finally { - // The disguised app must be uninstalled otherwise every following test will fail - await closeApp(device); - await runScriptAndLog( - `xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, - true - ); - } + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.USER_ACTIONS.APP_DISGUISE, async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disguise'), async () => { + await device.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'You have changed the icon for “Session”.', + }); + await device.clickOnElementAll({ + strategy: 'accessibility id', + selector: 'OK', + }); + }); + // TODO maybe grab a screenshot of the disguised app and see what you can do with it + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + await runScriptAndLog( + `xcrun simctl uninstall ${device.udid} com.loki-project.loki-messenger`, + true + ); + }); + } + }); } async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - await device.clickOnElementAll(new UserSettings(device)); - await device.clickOnElementAll(new AppearanceMenuItem(device)); - await device.clickOnElementAll(new SelectAppIcon(device)); - try { - await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); - await device.checkModalStrings( - englishStrippedStr('appIconAndNameChange').toString(), - englishStrippedStr('appIconAndNameChangeConfirmation').toString() - ); - await device.clickOnElementAll(new CloseAppButton(device)); - await sleepFor(2000); - // Open app library and check for disguised app - await device.swipeFromBottom(); - await device.waitForTextElementToBePresent(new DisguisedApp(device)); - } finally { - // The disguised app must be uninstalled otherwise every following test will fail - await closeApp(device); - await runScriptAndLog( - `${getAdbFullPath()} -s ${device.udid} uninstall network.loki.messenger.qa`, - true - ); - } + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.OPEN.APPEARANCE, async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new AppearanceMenuItem(device)); + }); + await test.step(TestSteps.USER_ACTIONS.APP_DISGUISE, async () => { + await device.clickOnElementAll(new SelectAppIcon(device)); + try { + await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disgusie'), async () => { + await device.checkModalStrings( + englishStrippedStr('appIconAndNameChange').toString(), + englishStrippedStr('appIconAndNameChangeConfirmation').toString() + ); + }); + await test.step('Verify app icon changed', async () => { + await device.clickOnElementAll(new CloseAppButton(device)); + await sleepFor(2000); + // Open app library and check for disguised app + await device.swipeFromBottom(); + await device.waitForTextElementToBePresent(new DisguisedApp(device)); + }); + } finally { + // The disguised app must be uninstalled otherwise every following test will fail + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + await runScriptAndLog( + `${getAdbFullPath()} -s ${device.udid} uninstall network.loki.messenger.qa`, + true + ); + }); + } + }); } diff --git a/run/types/allure.ts b/run/types/allure.ts index 1dfd195c9..1bd137bac 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -75,10 +75,12 @@ export const TestSteps = { OPEN: { UPDATE_GROUP_INFO: `Open 'Update Group Information' modal`, PATH: 'Open Path screen', + APPEARANCE: 'Open Appearance settings', }, // User Actions USER_ACTIONS: { CHANGE_PROFILE_PICTURE: 'Change profile picture', + APP_DISGUISE: 'Set App Disguise', }, // Disappearing Messages DISAPPEARING_MESSAGES: { @@ -90,6 +92,8 @@ export const TestSteps = { }, // Verify steps VERIFY: { + ELEMENT_SCREENSHOT: (elementDesc: string) => + `Verify ${elementDesc} element screenshot matches baseline`, GENERIC_MODAL: 'Verify modal strings', SPECIFIC_MODAL: (modalDesc: string) => `Verify ${modalDesc} modal strings`, MESSAGE_RECEIVED: 'Verify message has been received', From fc79d743678ea6d93e4d996dbaaaec13c61cfa94 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 10:36:41 +1000 Subject: [PATCH 15/88] fix: use ax id for conversationitem --- run/test/specs/locators/home.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index 1b26d734f..9829b0b2e 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -42,11 +42,6 @@ export class ConversationItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'pro-badge-text', - text: this.text, - } as const; case 'ios': return { strategy: 'accessibility id', From 363958743564d7b9f3592669e9da2b53c21212a7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 11:56:19 +1000 Subject: [PATCH 16/88] feat: add emoji react send tests --- run/test/specs/group_reaction.spec.ts | 64 +++++++++++++++++++++++++ run/test/specs/locators/conversation.ts | 52 ++++++++++++++++++++ run/test/specs/message_reaction.spec.ts | 56 ++++++++++++++++++++++ run/types/allure.ts | 6 ++- run/types/testing.ts | 5 ++ 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 run/test/specs/group_reaction.spec.ts create mode 100644 run/test/specs/message_reaction.spec.ts diff --git a/run/test/specs/group_reaction.spec.ts b/run/test/specs/group_reaction.spec.ts new file mode 100644 index 000000000..496a450f6 --- /dev/null +++ b/run/test/specs/group_reaction.spec.ts @@ -0,0 +1,64 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { GROUPNAME } from '../../types/testing'; +import { EmojiReactsCount, EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Send emoji react groups', + risk: 'high', + countOfDevicesNeeded: 3, + testCb: sendEmojiReactionGroup, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: 'Verifies that an emoji reaction can be sent and is received in a group.', +}); + +async function sendEmojiReactionGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const groupName: GROUPNAME = 'Message checks for groups'; + const message = 'Testing emoji reacts'; + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + focusGroupConvo: true, + groupName: groupName, + testInfo, + }); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, groupName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await Promise.all( + [bob1, charlie1].map(async device => { + await device.longPressMessage(message); + await device.clickOnElementAll(new FirstEmojiReact(device)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await device.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }) + ); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + // All clients witness emoji and "2" count + await Promise.all( + [alice1, bob1, charlie1].map(async device => { + await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); + await device.waitForTextElementToBePresent(new EmojiReactsCount(device)); + }) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); +} diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index d3a67f5ed..df7a1e1f3 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -437,3 +437,55 @@ export class PreferredDisplayName extends LocatorsInterface { } } } + +export class FirstEmojiReact extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger.qa:id/reaction_1', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: '😂', + } as const; + } + } +} + +export class EmojiReactsPill extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger.qa:id/layout_emoji_container', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: '😂', + } as const; + } + } +} + +export class EmojiReactsCount extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger.qa:id/reactions_pill_count', + text: '2', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: '2', + } as const; + } + } +} diff --git a/run/test/specs/message_reaction.spec.ts b/run/test/specs/message_reaction.spec.ts new file mode 100644 index 000000000..0c3eb9c6c --- /dev/null +++ b/run/test/specs/message_reaction.spec.ts @@ -0,0 +1,56 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_friends } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Send emoji react 1:1', + risk: 'high', + countOfDevicesNeeded: 2, + testCb: sendEmojiReaction, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: + 'Verifies that an emoji reaction can be sent and is received in a 1-1 conversation.', +}); + +async function sendEmojiReaction(platform: SupportedPlatformsType, testInfo: TestInfo) { + const message = 'Testing emoji reacts'; + const { + devices: { alice1, bob1 }, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: true, + testInfo, + }); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await bob1.longPressMessage(message); + await bob1.clickOnElementAll(new FirstEmojiReact(bob1)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await bob1.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + await Promise.all( + [alice1, bob1].map(async device => { + await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); + }) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/types/allure.ts b/run/types/allure.ts index 1bd137bac..5d5157914 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -25,7 +25,7 @@ export type AllureSuiteConfig = | { parent: 'In-App Review Prompt'; suite: 'Flows' | 'Triggers' } | { parent: 'Linkouts' } | { parent: 'New Conversation'; suite: 'Join Community' | 'New Message' } - | { parent: 'Sending Messages'; suite: 'Sending Attachments' } + | { parent: 'Sending Messages'; suite: 'Attachments' | 'Emoji reacts' } | { parent: 'Settings'; suite: 'App Disguise' } | { parent: 'User Actions'; @@ -68,8 +68,11 @@ export const TestSteps = { }, // Sending things SEND: { + MESSAGE: (sender: UserNameType, recipient: string) => + `${sender} sends a message to ${recipient}`, LINK: 'Send Link', IMAGE: 'Send Image', + EMOJI_REACT: `Send an emoji react`, }, // Open/Navigate steps OPEN: { @@ -103,5 +106,6 @@ export const TestSteps = { MISSED_CALL: 'Verify missed call', NICKNAME_CHANGED: (context: string) => `Verify nickname changed in/on ${context}`, PROFILE_PICTURE_CHANGED: 'Verify profile picture has been changed', + EMOJI_REACT: 'Verify emoji react appears for everyone', }, }; diff --git a/run/types/testing.ts b/run/types/testing.ts index 497e93198..bfb83dc8f 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -164,6 +164,8 @@ export type UiAutomatorQuery = export type AccessibilityId = | DISAPPEARING_TIMES | UserNameType + | '😂' + | '2' | 'Accept message request' | 'Accept name change' | 'Account ID' @@ -489,12 +491,15 @@ export type Id = | 'network.loki.messenger.qa:id/crop_image_menu_crop' | 'network.loki.messenger.qa:id/emptyStateContainer' | 'network.loki.messenger.qa:id/endCallButton' + | 'network.loki.messenger.qa:id/layout_emoji_container' | 'network.loki.messenger.qa:id/linkPreviewView' | 'network.loki.messenger.qa:id/mediapicker_folder_item_thumbnail' | 'network.loki.messenger.qa:id/mediapicker_image_item_thumbnail' | 'network.loki.messenger.qa:id/messageStatusTextView' | 'network.loki.messenger.qa:id/openGroupTitleTextView' | 'network.loki.messenger.qa:id/play_overlay' + | 'network.loki.messenger.qa:id/reaction_1' + | 'network.loki.messenger.qa:id/reactions_pill_count' | 'network.loki.messenger.qa:id/scrollToBottomButton' | 'network.loki.messenger.qa:id/search_cancel' | 'network.loki.messenger.qa:id/search_result_title' From b0e1f9ad3434bca26adc6750f4a0c4de8f41b585 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 11:56:41 +1000 Subject: [PATCH 17/88] fix: use whatever text is available for logging --- run/types/DeviceWrapper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 253f788f4..2041fd966 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1180,7 +1180,11 @@ export class DeviceWrapper { args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; - const { text, maxWait = 30_000 } = args; + + // Prefer text from args (if passed directly), otherwise check locator + const text = args.text ?? ('text' in locator ? locator.text : undefined); + + const { maxWait = 30_000 } = args; const description = describeLocator({ ...locator, text }); this.log(`Waiting for element with ${description} to be present`); From c6f27869e48764ad3e9e46252aeac584e6ba16fc Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 11:56:52 +1000 Subject: [PATCH 18/88] chore: rename allure suite --- run/test/specs/community_tests_image.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index 7c2c4e510..e39ef8c43 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -21,7 +21,7 @@ androidIt({ risk: 'medium', countOfDevicesNeeded: 2, testCb: sendImageCommunityAndroid, - allureSuites: { parent: 'Sending Messages', suite: 'Sending Attachments' }, + allureSuites: { parent: 'Sending Messages', suite: 'Attachments' }, allureDescription: 'Verifies that an image can be sent and received in a community', }); @@ -30,7 +30,7 @@ iosIt({ risk: 'medium', countOfDevicesNeeded: 1, testCb: sendImageCommunityIOS, - allureSuites: { parent: 'Sending Messages', suite: 'Sending Attachments' }, + allureSuites: { parent: 'Sending Messages', suite: 'Attachments' }, allureDescription: `Verifies that an image can be sent to a community. Note that due to Appium's limitations, this test does not verify another device receiving the image.`, }); From cf265af5d5298ae7504babdbc6f83c42748c5f1d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 13:45:58 +1000 Subject: [PATCH 19/88] fix: community input is now an id --- run/test/specs/locators/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 9b438fc50..3c95f5271 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -279,7 +279,7 @@ export class CommunityInput extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Community input', }; case 'ios': From c76563b02930b7fbbb990b4ee91fcb1bb5008e66 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 13:46:13 +1000 Subject: [PATCH 20/88] fix: path menu item is now an uiscrollable --- run/test/specs/locators/settings.ts | 4 ++-- run/types/testing.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index eb66ff200..395b7e2e0 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -298,8 +298,8 @@ export class PathMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'xpath', - selector: `//android.widget.TextView[@text="Path"]`, + strategy: '-android uiautomator', + selector: 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' } as const; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index bfb83dc8f..61bde4aca 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -155,6 +155,7 @@ export type XPath = export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().text("Enter your display name")' @@ -199,7 +200,6 @@ export type AccessibilityId = | 'Clear all' | 'Close' | 'Close button' - | 'Community input' | 'Community invitation' | 'Configuration message' | 'Confirm' @@ -427,6 +427,7 @@ export type Id = | 'com.android.settings:id/switch_text' | 'com.google.android.apps.photos:id/sign_in_button' | 'com.google.android.apps.photos:id/text' + | 'Community input' | 'Confirm invite button' | 'Contact' | 'Contact status' From a3d8eb00af557a13ee9b1ad2847cd466cad27437 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 15:39:24 +1000 Subject: [PATCH 21/88] feat: add community emoji react test --- run/test/specs/community_emoji_react.spec.ts | 62 ++++++++++++++++++++ run/test/specs/locators/index.ts | 2 +- run/test/specs/locators/settings.ts | 3 +- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 run/test/specs/community_emoji_react.spec.ts diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts new file mode 100644 index 000000000..e4337f1ee --- /dev/null +++ b/run/test/specs/community_emoji_react.spec.ts @@ -0,0 +1,62 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; +import { open_Alice1_Bob1_friends } from './state_builder'; +import { joinCommunity } from './utils/join_community'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +// Communities are too unreliable on iOS at the moment, see SES-4425 +androidIt({ + title: 'Send emoji react community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: sendEmojiReactionCommunity, + allureSuites: { + parent: 'Sending Messages', + suite: 'Emoji reacts', + }, + allureDescription: 'Verifies that an emoji reaction can be sent and is received in a community', +}); + +async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + const message = `Testing emoji reacts - ${new Date().getTime()} - ${platform}`; + const { + devices: { alice1, bob1 }, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: false, + testInfo, + }); + }); + await Promise.all( + [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + ); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await alice1.sendMessage(message); + }); + await test.step(TestSteps.SEND.EMOJI_REACT, async () => { + await bob1.scrollToBottom(); + await bob1.longPressMessage(message); + await bob1.clickOnElementAll(new FirstEmojiReact(bob1)); + // Verify long press menu disappeared (so next found emoji is in convo and not in react bar) + await bob1.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Reply to message', + }); + }); + await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { + await Promise.all( + [alice1, bob1].map(async device => { + await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); + }) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 3c95f5271..af4b5888a 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -262,7 +262,7 @@ export class JoinCommunityButton extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Join community button', }; case 'ios': diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 395b7e2e0..f2f9bcf80 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -299,7 +299,8 @@ export class PathMenuItem extends LocatorsInterface { case 'android': return { strategy: '-android uiautomator', - selector: 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', } as const; case 'ios': return { From 4a0a2b3b9dbff1cf255acbfd0ed1c85d0461e0eb Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 16:18:30 +1000 Subject: [PATCH 22/88] feat: allow links for known bugs in allure reports --- run/types/sessionIt.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 81dd43a65..3de9973c9 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -24,6 +24,11 @@ type MobileItArgs = { shouldSkip?: boolean; allureSuites?: AllureSuiteConfig; allureDescription?: string; + allureLinks?: { + all?: string[] | string; + android?: string[] | string; + ios?: string[] | string; + }; }; export function androidIt(args: Omit) { @@ -43,6 +48,7 @@ function mobileIt({ countOfDevicesNeeded, allureSuites, allureDescription, + allureLinks, }: MobileItArgs) { const testName = `${title} @${platform} @${risk ?? 'default'}-risk @${countOfDevicesNeeded}-devices`; @@ -67,6 +73,25 @@ function mobileIt({ if (allureDescription) { await allure.description(allureDescription); } + if (allureLinks) { + const allLinks = allureLinks.all + ? Array.isArray(allureLinks.all) + ? allureLinks.all + : [allureLinks.all] + : []; + + const platformLinks = allureLinks[platform] + ? Array.isArray(allureLinks[platform]) + ? allureLinks[platform] + : [allureLinks[platform]] + : []; + + const links = [...allLinks, ...platformLinks]; + + for (const jiraKey of links) { + await allure.link(`https://optf.atlassian.net/browse/${jiraKey}`, jiraKey, 'issue'); + } + } let testFailed = false; try { From 6899783a660239effd0e34a31f932e02dd039fe7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 16:21:09 +1000 Subject: [PATCH 23/88] feat: add in allure links for current known issues --- run/test/specs/message_long_text.spec.ts | 3 +++ run/test/specs/user_actions_change_username.spec.ts | 3 +++ run/test/specs/user_actions_set_nickname.spec.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index a86772ba6..e23909635 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -17,6 +17,9 @@ bothPlatformsItSeparate({ android: { testCb: sendLongMessageAndroid, }, + allureLinks: { + android: 'SES-4337' + } }); async function sendLongMessageIos(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index 1e08eddc5..ad025cf4e 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -13,6 +13,9 @@ bothPlatformsIt({ risk: 'medium', countOfDevicesNeeded: 1, testCb: changeUsername, + allureLinks: { + android: 'SES-4277' + } }); async function changeUsername(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index 253ee7a4c..e84c2dce8 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -26,6 +26,9 @@ bothPlatformsIt({ suite: 'Set Nickname', }, allureDescription: `Verifies that a user can set a nickname for a contact and that it appears correctly in the conversation settings, conversation header and home screen.`, + allureLinks: { + android: 'SES-4424' + } }); async function setNickname(platform: SupportedPlatformsType, testInfo: TestInfo) { From cc68a5401f1a0d012dca8a33af799c7dbaec3ed6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 16:58:52 +1000 Subject: [PATCH 24/88] fix: move allure test info to helper file --- run/test/specs/message_long_text.spec.ts | 4 +- .../user_actions_change_username.spec.ts | 4 +- .../specs/user_actions_set_nickname.spec.ts | 4 +- run/test/specs/utils/allure/allureHelpers.ts | 48 +++++++++++++++++++ run/types/sessionIt.ts | 36 ++++---------- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index e23909635..643906d82 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -18,8 +18,8 @@ bothPlatformsItSeparate({ testCb: sendLongMessageAndroid, }, allureLinks: { - android: 'SES-4337' - } + android: 'SES-4337', + }, }); async function sendLongMessageIos(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index ad025cf4e..0401da51d 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -14,8 +14,8 @@ bothPlatformsIt({ countOfDevicesNeeded: 1, testCb: changeUsername, allureLinks: { - android: 'SES-4277' - } + android: 'SES-4277', + }, }); async function changeUsername(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index e84c2dce8..5ba9d51d6 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -27,8 +27,8 @@ bothPlatformsIt({ }, allureDescription: `Verifies that a user can set a nickname for a contact and that it appears correctly in the conversation settings, conversation header and home screen.`, allureLinks: { - android: 'SES-4424' - } + android: 'SES-4424', + }, }); async function setNickname(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 2ccaa6225..eeabca5ad 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -1,3 +1,4 @@ +import * as allure from 'allure-js-commons'; import { execSync } from 'child_process'; import fs from 'fs-extra'; import { glob } from 'glob'; @@ -8,6 +9,7 @@ import { allureResultsDir, GH_PAGES_BASE_URL, } from '../../../../constants/allure'; +import { AllureSuiteConfig } from '../../../../types/allure'; import { SupportedPlatformsType } from '../open_app'; export interface ReportContext { @@ -177,3 +179,49 @@ function getGitCommitSha(): string { function getGitBranch(): string { return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); } +// Handle test-level metadata such as suites, test description or linked issues +export async function setupAllureTestInfo({ + suites, + description, + links, + platform, +}: { + suites?: AllureSuiteConfig; + description?: string; + links?: { + all?: string[] | string; // Bugs affecting both platforms + android?: string[] | string; // Android only - won't appear in iOS reports + ios?: string[] | string; // iOS only - won't appear in Android reports + }; + platform?: 'android' | 'ios'; +}) { + // Handle suites + if (suites) { + await allure.parentSuite(suites.parent); + if ('suite' in suites) { + await allure.suite(suites.suite); + } + } + + // Handle description + if (description) { + await allure.description(description); + } + + // Handle links (only process if platform is provided) + if (links && platform) { + const allLinks = links.all ? (Array.isArray(links.all) ? links.all : [links.all]) : []; + + const platformLinks = links[platform] + ? Array.isArray(links[platform]) + ? links[platform] + : [links[platform]] + : []; + + const combinedLinks = [...allLinks, ...platformLinks]; + + for (const jiraKey of combinedLinks) { + await allure.link(`https://optf.atlassian.net/browse/${jiraKey}`, jiraKey, 'issue'); + } + } +} diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 3de9973c9..5e56972be 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -1,10 +1,10 @@ // run/types/sessionIt.ts - Clean version matching original pattern import { test, type TestInfo } from '@playwright/test'; -import * as allure from 'allure-js-commons'; import { omit } from 'lodash'; import type { AppCountPerTest } from '../test/specs/state_builder'; +import { setupAllureTestInfo } from '../test/specs/utils/allure/allureHelpers'; import { getNetworkTarget } from '../test/specs/utils/devnet'; import { SupportedPlatformsType } from '../test/specs/utils/open_app'; import { @@ -64,34 +64,14 @@ function mobileIt({ getNetworkTarget(platform); console.info(`\n\n==========> Running "${testName}"\n\n`); - if (allureSuites) { - await allure.parentSuite(allureSuites.parent); - if ('suite' in allureSuites) { - await allure.suite(allureSuites.suite); - } - } - if (allureDescription) { - await allure.description(allureDescription); - } - if (allureLinks) { - const allLinks = allureLinks.all - ? Array.isArray(allureLinks.all) - ? allureLinks.all - : [allureLinks.all] - : []; - - const platformLinks = allureLinks[platform] - ? Array.isArray(allureLinks[platform]) - ? allureLinks[platform] - : [allureLinks[platform]] - : []; - - const links = [...allLinks, ...platformLinks]; + // Handle Suites, Descriptions and Links + await setupAllureTestInfo({ + suites: allureSuites, + description: allureDescription, + links: allureLinks, + platform, + }); - for (const jiraKey of links) { - await allure.link(`https://optf.atlassian.net/browse/${jiraKey}`, jiraKey, 'issue'); - } - } let testFailed = false; try { From dfab494d6d93726bad54d6f9d55c7d73a2ade6f7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 17:01:36 +1000 Subject: [PATCH 25/88] chore: fix step description --- run/test/specs/community_emoji_react.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index e4337f1ee..f6c245acf 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -25,7 +25,7 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test const message = `Testing emoji reacts - ${new Date().getTime()} - ${platform}`; const { devices: { alice1, bob1 }, - prebuilt: { alice, bob }, + prebuilt: { alice }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return open_Alice1_Bob1_friends({ platform, @@ -36,7 +36,7 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test await Promise.all( [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) ); - await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { await alice1.sendMessage(message); }); await test.step(TestSteps.SEND.EMOJI_REACT, async () => { From b534e46bfe4d980014c22f5291d0ebf6cf85b833 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 21 Aug 2025 17:04:51 +1000 Subject: [PATCH 26/88] chore: remove SES link --- run/test/specs/linked_device_hide_note_to_self.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 914a74cfa..1ca559ba1 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 @@ -46,7 +46,7 @@ async function hideNoteToSelf(platform: SupportedPlatformsType, testInfo: TestIn await test.step(TestSteps.VERIFY.GENERIC_MODAL, async () => { await alice1.checkModalStrings( englishStrippedStr('noteToSelfHide').toString(), - englishStrippedStr('hideNoteToSelfDescription').toString(), // This one fails on iOS, see SES-4144 + englishStrippedStr('hideNoteToSelfDescription').toString(), false ); }); From f3aa4c36137562227d726517aea0582cd7606068 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 22 Aug 2025 11:00:25 +1000 Subject: [PATCH 27/88] feat: community poll limit on ios --- run/test/specs/community_emoji_react.spec.ts | 5 +- run/test/specs/community_tests_image.spec.ts | 52 +++----------------- run/test/specs/utils/capabilities_ios.ts | 1 + run/types/DeviceWrapper.ts | 2 +- 4 files changed, 12 insertions(+), 48 deletions(-) diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index f6c245acf..5355fca5d 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -2,14 +2,13 @@ import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { EmojiReactsPill, FirstEmojiReact } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { joinCommunity } from './utils/join_community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// Communities are too unreliable on iOS at the moment, see SES-4425 -androidIt({ +bothPlatformsIt({ title: 'Send emoji react community', risk: 'medium', countOfDevicesNeeded: 2, diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index e39ef8c43..e2c834ae0 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -2,40 +2,22 @@ import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; import { TestSteps } from '../../types/allure'; -import { androidIt, iosIt } from '../../types/sessionIt'; -import { USERNAME } from '../../types/testing'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; -import { newUser } from './utils/create_account'; import { joinCommunity } from './utils/join_community'; -import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// NOTE For some reason Appium takes FOREVER to load the iOS page source of the community on the recipients device -// and as such I haven't found a quick and easy way to verify that they see the new image message -// If this becomes a problem in the future then we can extract the unread count from page source and see it increment after the image gets sent -// But for now we have to trust that the sender seeing 'Sent' also delivers it to others on iOS -// This is also why it's a 1-device test and has its own iosIt definition (and not bothPlatformsItSeparate) - -androidIt({ +bothPlatformsIt({ title: 'Send image to community', risk: 'medium', countOfDevicesNeeded: 2, - testCb: sendImageCommunityAndroid, + testCb: sendImageCommunity, allureSuites: { parent: 'Sending Messages', suite: 'Attachments' }, allureDescription: 'Verifies that an image can be sent and received in a community', }); -iosIt({ - title: 'Send image to community', - risk: 'medium', - countOfDevicesNeeded: 1, - testCb: sendImageCommunityIOS, - allureSuites: { parent: 'Sending Messages', suite: 'Attachments' }, - allureDescription: `Verifies that an image can be sent to a community. - Note that due to Appium's limitations, this test does not verify another device receiving the image.`, -}); - -async function sendImageCommunityAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { @@ -57,10 +39,10 @@ async function sendImageCommunityAndroid(platform: SupportedPlatformsType, testI await alice1.sendImage(testImageMessage, true); }); await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { - await sleepFor(5000); // Give bob some time to receive the message so the test doesn't scroll down too early + await sleepFor(2000); // Give bob some time to receive the image await bob1.scrollToBottom(); - await bob1.trustAttachments(testCommunityName); - await bob1.scrollToBottom(); // Gotta keep scrolling down to make sure we're at the very bottom + await bob1.onAndroid().trustAttachments(testCommunityName); + await bob1.onAndroid().scrollToBottom(); // Trusting attachments scrolls the viewport up a bit so gotta scroll to bottom again await bob1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Message body', @@ -72,21 +54,3 @@ async function sendImageCommunityAndroid(platform: SupportedPlatformsType, testI await closeApp(alice1, bob1); }); } -async function sendImageCommunityIOS(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - await newUser(device, USERNAME.ALICE, { saveUserData: false }); - return { device }; - }); - const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; - await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { - await joinCommunity(device, testCommunityLink, testCommunityName); - }); - await test.step(TestSteps.SEND.IMAGE, async () => { - await device.sendImage(testImageMessage, true); - }); - - await test.step(TestSteps.SETUP.CLOSE_APP, async () => { - await closeApp(device); - }); -} diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 5d835f95d..87bf203bd 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -28,6 +28,7 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { 'appium:processArguments': { env: { debugDisappearingMessageDurations: 'true', + communityPollLimit: 5, }, }, // "appium:isHeadless": true, diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2041fd966..e8731faeb 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -892,7 +892,7 @@ export class DeviceWrapper { } } catch (err) { // If matching fails for this element, log and continue to the next - this.warn( + this.info( `[matchAndTapImage] Matching failed for element ${i + 1}:`, err instanceof Error ? err.message : err ); From 1224668f0feb9f6e539de4301e85e534b3a43c35 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 22 Aug 2025 11:57:14 +1000 Subject: [PATCH 28/88] refactor!: scrub all occurrences of message body selector --- run/test/specs/community_emoji_react.spec.ts | 6 +- run/test/specs/community_tests_image.spec.ts | 12 +- run/test/specs/disappear_after_read.spec.ts | 26 ++--- run/test/specs/disappear_after_send.spec.ts | 25 ++--- .../specs/disappear_after_send_groups.spec.ts | 5 +- .../disappear_after_send_note_to_self.spec.ts | 5 +- run/test/specs/disappearing_image.spec.ts | 5 +- run/test/specs/disappearing_link.spec.ts | 11 +- run/test/specs/disappearing_video.spec.ts | 5 +- .../group_disappearing_messages_image.spec.ts | 5 +- .../group_disappearing_messages_link.spec.ts | 11 +- .../group_disappearing_messages_video.spec.ts | 5 +- run/test/specs/group_message_delete.spec.ts | 36 ++---- run/test/specs/group_message_document.spec.ts | 43 +++----- run/test/specs/group_message_gif.spec.ts | 31 ++---- run/test/specs/group_message_image.spec.ts | 66 +++++------ .../specs/group_message_link_preview.spec.ts | 54 ++++----- .../specs/group_message_long_text.spec.ts | 104 +++--------------- run/test/specs/group_message_unsend.spec.ts | 19 +--- run/test/specs/group_message_video.spec.ts | 54 +++------ run/test/specs/group_message_voice.spec.ts | 66 ++--------- run/test/specs/group_tests_mentions.spec.ts | 7 +- .../linked_device_delete_message.spec.ts | 19 +--- .../specs/linked_device_restore_group.spec.ts | 26 +---- run/test/specs/locators/conversation.ts | 19 ++++ run/test/specs/message_deletion.spec.ts | 14 +-- run/test/specs/message_document.spec.ts | 15 +-- run/test/specs/message_gif.spec.ts | 54 +-------- run/test/specs/message_image.spec.ts | 14 +-- run/test/specs/message_link_preview.spec.ts | 20 ++-- run/test/specs/message_long_text.spec.ts | 7 +- run/test/specs/message_reaction.spec.ts | 6 +- run/test/specs/message_unsend.spec.ts | 8 +- run/test/specs/message_video.spec.ts | 20 +--- run/test/specs/message_voice.spec.ts | 8 +- ...actions_block_conversation_options.spec.ts | 8 +- .../user_actions_delete_contact_ucs.spec.ts | 7 +- .../specs/user_actions_read_status.spec.ts | 7 +- .../user_actions_share_to_session.spec.ts | 8 +- .../specs/user_actions_unblock_user.spec.ts | 8 +- run/test/specs/utils/create_contact.ts | 7 +- run/test/specs/utils/create_group.ts | 52 +++------ run/types/DeviceWrapper.ts | 14 +-- 43 files changed, 282 insertions(+), 660 deletions(-) diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index 5355fca5d..bcc656564 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -50,9 +50,9 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test }); await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { await Promise.all( - [alice1, bob1].map(async device => { - await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); - }) + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new EmojiReactsPill(device)) + ) ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index e2c834ae0..6aca09d0a 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -3,6 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { joinCommunity } from './utils/join_community'; @@ -30,9 +31,7 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( - [alice1, bob1].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); - }) + [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) ); }); await test.step(TestSteps.SEND.IMAGE, async () => { @@ -43,13 +42,8 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te await bob1.scrollToBottom(); await bob1.onAndroid().trustAttachments(testCommunityName); await bob1.onAndroid().scrollToBottom(); // Trusting attachments scrolls the viewport up a bit so gotta scroll to bottom again - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testImageMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testImageMessage)); }); - await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); }); diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index aeabf7bd2..ee22115d6 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { checkDisappearingControlMessage } from './utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -53,22 +54,15 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te ); // Send message to verify that deletion is working await alice1.sendMessage(testMessage); - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - ]); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementBeenDeleted({ + ...new MessageBody(device, testMessage).build(), + maxWait, + preventEarlyDeletion: true, + }) + ) + ); // Great success await closeApp(alice1, bob1); } diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index bebfb5226..709cec7de 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { checkDisappearingControlMessage } from './utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -53,21 +54,15 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te // Send message to verify that deletion is working await alice1.sendMessage(testMessage); // Wait for message to disappear - await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait, - }), - ]); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementBeenDeleted({ + ...new MessageBody(device, testMessage).build(), + maxWait, + preventEarlyDeletion: true, + }) + ) + ); // Great success await closeApp(alice1, bob1); diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 8c711e8f5..3c32a7d36 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -3,6 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -56,10 +57,8 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testMessage).build(), maxWait, - text: testMessage, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/disappear_after_send_note_to_self.spec.ts b/run/test/specs/disappear_after_send_note_to_self.spec.ts index 896432dd9..dc38c8bea 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 @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { PlusButton } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { sleepFor } from './utils'; @@ -49,9 +50,7 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, te ); await device.sendMessage(testMessage); await device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, + ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, }); diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index cce7de960..d40cf53a8 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -37,10 +38,8 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes await alice1.sendImage(testMessage); await Promise.all([ alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(alice1, testMessage).build(), maxWait, - text: testMessage, preventEarlyDeletion: true, }), bob1.hasElementBeenDeleted({ diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index 22c229e38..4a74d3d73 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -6,7 +6,12 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -72,10 +77,8 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t await Promise.all( [alice1, bob1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testLink).build(), maxWait, - text: testLink, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index cf0736a72..9f939cd25 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -41,11 +42,9 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes await Promise.all( [alice1, bob1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testMessage).build(), initialMaxWait, maxWait, - text: testMessage, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 41144319f..a388a8dd6 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -44,10 +45,8 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testMessage).build(), maxWait, - text: testMessage, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index d71f1fe04..fdd345e6e 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -6,7 +6,12 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -71,10 +76,8 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testLink).build(), maxWait, - text: testLink, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 31618ff30..8a7b698f7 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -45,10 +46,8 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Message body', + ...new MessageBody(device, testMessage).build(), maxWait, - text: testMessage, preventEarlyDeletion: true, }) ) diff --git a/run/test/specs/group_message_delete.spec.ts b/run/test/specs/group_message_delete.spec.ts index c4fff3034..e59be0fd5 100644 --- a/run/test/specs/group_message_delete.spec.ts +++ b/run/test/specs/group_message_delete.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageLocally } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,18 +25,11 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te testInfo, }); const sentMessage = await alice1.sendMessage('Checking local delete functionality'); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon @@ -52,17 +45,10 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Excellent // Check device 2 and 3 that message is still visible - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_document.spec.ts b/run/test/specs/group_message_document.spec.ts index 5f046e520..930302f78 100644 --- a/run/test/specs/group_message_document.spec.ts +++ b/run/test/specs/group_message_document.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -28,30 +29,19 @@ async function sendDocumentGroupiOS(platform: SupportedPlatformsType, testInfo: focusGroupConvo: true, testInfo, }); - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing-document'; const replyMessage = `Replying to document from ${alice.userName}`; await alice1.sendDocument(); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) + ) + ); await bob1.longPressMessage(testMessage); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1, charlie1); } @@ -91,18 +81,11 @@ async function sendDocumentGroupAndroid(platform: SupportedPlatformsType, testIn await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply from device 2 came through on alice1 and charlie1 - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app and server await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_gif.spec.ts b/run/test/specs/group_message_gif.spec.ts index dc9628311..fae04e543 100644 --- a/run/test/specs/group_message_gif.spec.ts +++ b/run/test/specs/group_message_gif.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -46,16 +47,11 @@ async function sendGifGroupiOS(platform: SupportedPlatformsType, testInfo: TestI // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } @@ -93,16 +89,11 @@ async function sendGifGroupAndroid(platform: SupportedPlatformsType, testInfo: T // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_image.spec.ts b/run/test/specs/group_message_image.spec.ts index 14cd14d76..92be52112 100644 --- a/run/test/specs/group_message_image.spec.ts +++ b/run/test/specs/group_message_image.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { OutgoingMessageStatusSent } from './locators/conversation'; +import { MessageBody, OutgoingMessageStatusSent } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -36,35 +36,23 @@ async function sendImageGroupiOS(platform: SupportedPlatformsType, testInfo: Tes ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 50000, }); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, testMessage).build(), + maxWait: 5_000, + }) + ) + ); const replyMessage = await bob1.replyToMessage(alice, testMessage); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - maxWait: 5000, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - maxWait: 5000, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, replyMessage).build(), + maxWait: 5_000, + }) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } @@ -106,18 +94,14 @@ async function sendImageGroupAndroid(platform: SupportedPlatformsType, testInfo: await bob1.longPress('Media message'); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new MessageBody(device, replyMessage).build(), + maxWait: 5_000, + }) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_link_preview.spec.ts b/run/test/specs/group_message_link_preview.spec.ts index 2d01006b4..3652a12c3 100644 --- a/run/test/specs/group_message_link_preview.spec.ts +++ b/run/test/specs/group_message_link_preview.spec.ts @@ -4,7 +4,12 @@ import { testLink } from '../../constants'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -52,30 +57,20 @@ async function sendLinkGroupiOS(platform: SupportedPlatformsType, testInfo: Test await alice1.inputText(testLink, new MessageInput(alice1)); await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testLink)) + ) + ); // Reply to link await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } @@ -115,17 +110,10 @@ async function sendLinkGroupAndroid(platform: SupportedPlatformsType, testInfo: await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); const replyMessage = await bob1.sendMessage(`${alice.userName} message reply`); - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index d5c7ad9ae..70da9c211 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -1,29 +1,21 @@ import type { TestInfo } from '@playwright/test'; import { longText } from '../../constants'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { OutgoingMessageStatusSent } from './locators/conversation'; -import { ConversationItem } from './locators/home'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send long message to group', risk: 'low', countOfDevicesNeeded: 3, - ios: { - testCb: sendLongMessageGroupiOS, - }, - android: { - testCb: sendLongMessageGroupAndroid, - }, + testCb: sendLongMessageGroup, allureDescription: 'Verifies that a long message can be sent to a group', }); -async function sendLongMessageGroupiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendLongMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Message checks for groups'; - // Sending a long text message - const { devices: { alice1, bob1, charlie1 }, prebuilt: { alice }, @@ -34,82 +26,18 @@ async function sendLongMessageGroupiOS(platform: SupportedPlatformsType, testInf testInfo, }); await alice1.sendMessage(longText); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, longText)) + ) + ); const replyMessage = await bob1.replyToMessage(alice, longText); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Close app - await closeApp(alice1, bob1, charlie1); -} - -async function sendLongMessageGroupAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - const testGroupName = 'Message checks for groups'; - - const { - devices: { alice1, bob1, charlie1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, - }); - - // Sending a long text message - await alice1.sendMessage(longText); - await alice1.waitForTextElementToBePresent({ - ...new OutgoingMessageStatusSent(alice1).build(), - maxWait: 50000, - }); - - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: longText, - }), - ]); - await bob1.longPressMessage(longText); - await bob1.clickOnByAccessibilityID('Reply to message'); - const replyMessage = await bob1.sendMessage(`${alice.userName} message reply`); - // Go out and back into the group to see the last message - await alice1.navigateBack(); - await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Go out and back into the group to see the last message - await charlie1.navigateBack(); - await charlie1.clickOnElementAll(new ConversationItem(charlie1, testGroupName)); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(async device => { + await device.scrollToBottom(); + await device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)); + }) + ); // Close app await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_unsend.spec.ts b/run/test/specs/group_message_unsend.spec.ts index fb7420033..12384b49a 100644 --- a/run/test/specs/group_message_unsend.spec.ts +++ b/run/test/specs/group_message_unsend.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,18 +26,11 @@ async function unsendMessageGroup(platform: SupportedPlatformsType, testInfo: Te testInfo, }); const sentMessage = await alice1.sendMessage('Checking unsend functionality'); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon diff --git a/run/test/specs/group_message_video.spec.ts b/run/test/specs/group_message_video.spec.ts index af098bd2f..8cc670414 100644 --- a/run/test/specs/group_message_video.spec.ts +++ b/run/test/specs/group_message_video.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -31,35 +32,19 @@ async function sendVideoGroupiOS(platform: SupportedPlatformsType, testInfo: Tes const testMessage = 'Testing-video-1'; const replyMessage = `Replying to video from ${alice.userName} in ${testGroupName}`; await alice1.sendVideoiOS(testMessage); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - maxWait: 5000, - }); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) + ) + ); await bob1.longPressMessage(testMessage); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close server and devices await closeApp(alice1, bob1, charlie1); } @@ -110,18 +95,11 @@ async function sendVideoGroupAndroid(platform: SupportedPlatformsType, testInfo: await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply appears in device 1 and device 3 - await Promise.all([ - alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }), - ]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) + ) + ); // Close app and server await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index 9c7d342be..f3ce8bcec 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -1,22 +1,18 @@ import type { TestInfo } from '@playwright/test'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send voice message to group', risk: 'high', countOfDevicesNeeded: 3, - ios: { - testCb: sendVoiceMessageGroupiOS, - }, - android: { - testCb: sendVoiceMessageGroupAndroid, - }, + testCb: sendVoiceMessageGroup, }); -async function sendVoiceMessageGroupiOS(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Message checks for groups'; const { devices: { alice1, bob1, charlie1 }, @@ -30,49 +26,8 @@ async function sendVoiceMessageGroupiOS(platform: SupportedPlatformsType, testIn const replyMessage = `Replying to voice message from ${alice.userName} in ${testGroupName}`; await alice1.sendVoiceMessage(); await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }) - ) - ); - await bob1.longPress('Voice message'); - await bob1.clickOnByAccessibilityID('Reply to message'); - await bob1.sendMessage(replyMessage); - await Promise.all( - [alice1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }) - ) + [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); - // Close server and devices - await closeApp(alice1, bob1, charlie1); -} - -async function sendVoiceMessageGroupAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // open devices - const testGroupName = 'Message checks for groups'; - const { - devices: { alice1, bob1, charlie1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, - }); - const replyMessage = `Replying to voice message from ${alice.userName} in ${testGroupName}`; - // Select voice message button to activate recording state - await alice1.sendVoiceMessage(); - await Promise.all([ - bob1.trustAttachments(testGroupName), - charlie1.trustAttachments(testGroupName), - ]); - // Check device 2 and 3 for voice message from user A await Promise.all( [alice1, bob1, charlie1].map(device => device.waitForTextElementToBePresent({ @@ -81,19 +36,14 @@ async function sendVoiceMessageGroupAndroid(platform: SupportedPlatformsType, te }) ) ); - // Reply to voice message await bob1.longPress('Voice message'); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - // Check device 1 and 3 for reply to appear await Promise.all( [alice1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }) + device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) ) ); + // Close server and devices await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/group_tests_mentions.spec.ts b/run/test/specs/group_tests_mentions.spec.ts index 7abf2e39f..e045f88a3 100644 --- a/run/test/specs/group_tests_mentions.spec.ts +++ b/run/test/specs/group_tests_mentions.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,11 +26,7 @@ async function mentionsForGroups(platform: SupportedPlatformsType, testInfo: Tes await alice1.mentionContact(platform, bob); // Check format on User B's device - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: `@You`, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, '@You')); // await device2.findMessageWithBody(`@You`); // Bob to Select User C await bob1.mentionContact(platform, charlie); diff --git a/run/test/specs/linked_device_delete_message.spec.ts b/run/test/specs/linked_device_delete_message.spec.ts index c4a0822a0..97f35cf82 100644 --- a/run/test/specs/linked_device_delete_message.spec.ts +++ b/run/test/specs/linked_device_delete_message.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -39,18 +39,11 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test // Check linked device for deleted message await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Check device 2 and 3 for no change - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - alice2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }), - ]); + await Promise.all( + [bob1, alice2].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + ) + ); // Close app await closeApp(alice1, bob1, alice2); } diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index fadea66f7..2c77a9faf 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationHeaderName } from './locators/conversation'; +import { ConversationHeaderName, MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; @@ -35,31 +35,15 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo await device4.waitForTextElementToBePresent(new ConversationHeaderName(device4, testGroupName)); // Check all messages are present await Promise.all([ - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), + device4.waitForTextElementToBePresent(new MessageBody(device4, aliceMessage)), + device4.waitForTextElementToBePresent(new MessageBody(device4, bobMessage)), + device4.waitForTextElementToBePresent(new MessageBody(device4, charlieMessage)), ]); const testMessage2 = 'Checking that message input is working'; await device4.sendMessage(testMessage2); await Promise.all( [device1, device2, device3].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage2, - }) + device.waitForTextElementToBePresent(new MessageBody(device, testMessage2)) ) ); await closeApp(device1, device2, device3, device4); diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index df7a1e1f3..763b9a6e4 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -23,6 +23,25 @@ export class SendButton extends LocatorsInterface { } } +export class MessageBody extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Message body', + text: this.text, + } as const; + } + } +} + export class ScrollToBottomButton extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/message_deletion.spec.ts b/run/test/specs/message_deletion.spec.ts index f25df4c07..76c3059f3 100644 --- a/run/test/specs/message_deletion.spec.ts +++ b/run/test/specs/message_deletion.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageLocally } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -23,11 +23,7 @@ async function deleteMessage(platform: SupportedPlatformsType, testInfo: TestInf }); // send message from User A to User B const sentMessage = await alice1.sendMessage('Checking local deletetion functionality'); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); // Select Delete icon @@ -44,11 +40,7 @@ async function deleteMessage(platform: SupportedPlatformsType, testInfo: TestInf await alice1.waitForTextElementToBePresent(new DeletedMessage(alice1)); // Device 2 should show no change - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); // Excellent await closeApp(alice1, bob1); diff --git a/run/test/specs/message_document.spec.ts b/run/test/specs/message_document.spec.ts index 60c7b0934..fc7422e7c 100644 --- a/run/test/specs/message_document.spec.ts +++ b/run/test/specs/message_document.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -19,17 +20,13 @@ async function sendDocument(platform: SupportedPlatformsType, testInfo: TestInfo focusFriendsConvo: true, testInfo, }); - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing documents'; const replyMessage = `Replying to document from ${alice.userName}`; await alice1.sendDocument(); await bob1.trustAttachments(alice.userName); // Reply to message - await bob1.onIOS().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.onIOS().waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); await bob1.onAndroid().waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Document', @@ -38,11 +35,7 @@ async function sendDocument(platform: SupportedPlatformsType, testInfo: TestInfo await bob1.onAndroid().longPress('Document'); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_gif.spec.ts b/run/test/specs/message_gif.spec.ts index 1e7e08aed..1c8c8e887 100644 --- a/run/test/specs/message_gif.spec.ts +++ b/run/test/specs/message_gif.spec.ts @@ -1,23 +1,18 @@ import type { TestInfo } from '@playwright/test'; -import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; -import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsItSeparate({ +bothPlatformsIt({ title: 'Send GIF 1:1', risk: 'medium', countOfDevicesNeeded: 2, - ios: { - testCb: sendGifIos, - }, - android: { - testCb: sendGifAndroid, - }, + testCb: sendGif, }); -async function sendGifIos(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function sendGif(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, prebuilt: { alice }, @@ -37,44 +32,7 @@ async function sendGifIos(platform: SupportedPlatformsType, testInfo: TestInfo) await bob1.longPress('Media message'); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - // Close app - await closeApp(alice1, bob1); -} - -async function sendGifAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // Test sending a video - // open devices and server - const { - devices: { alice1, bob1 }, - prebuilt: { alice }, - } = await open_Alice1_Bob1_friends({ - platform, - focusFriendsConvo: true, - testInfo, - }); - const replyMessage = `Replying to GIF from ${alice.userName}`; - // Click on attachments button - await alice1.sendGIF(); - // Check if the 'Tap to download media' config appears - // Click on config - await bob1.trustAttachments(alice.userName); - // Reply to message - await sleepFor(5000); - await bob1.longPress('Media message'); - // Check reply came through on alice1 - await bob1.clickOnByAccessibilityID('Reply to message'); - await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_image.spec.ts b/run/test/specs/message_image.spec.ts index 22d5ceaa1..17eed7e09 100644 --- a/run/test/specs/message_image.spec.ts +++ b/run/test/specs/message_image.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,19 +27,10 @@ async function sendImage(platform: SupportedPlatformsType, testInfo: TestInfo) { await alice1.sendImage(testMessage); // Trust message on device 2 (bob) await bob1.trustAttachments(alice.userName); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); // Reply to message (on device 2 - Bob) const replyMessage = await bob1.replyToMessage(bob, testMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_link_preview.spec.ts b/run/test/specs/message_link_preview.spec.ts index 075b47679..d80076f55 100644 --- a/run/test/specs/message_link_preview.spec.ts +++ b/run/test/specs/message_link_preview.spec.ts @@ -4,7 +4,12 @@ import { testLink } from '../../constants'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { LinkPreview, LinkPreviewMessage } from './locators'; -import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -51,20 +56,11 @@ async function sendLinkIos(platform: SupportedPlatformsType, testInfo: TestInfo) await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); await alice1.clickOnElementAll(new SendButton(alice1)); // Make sure image preview is available in device 2 - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testLink, - }); - + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testLink)); await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index 643906d82..1bde31726 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { longText } from '../../constants'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -62,11 +63,7 @@ async function sendLongMessageAndroid(platform: SupportedPlatformsType, testInfo // Reply to message (User B to User A) const sentMessage = await bob1.replyToMessage(alice, longText); // Check reply came through on alice1 - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, sentMessage)); // Close app await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_reaction.spec.ts b/run/test/specs/message_reaction.spec.ts index 0c3eb9c6c..aa7b4b78a 100644 --- a/run/test/specs/message_reaction.spec.ts +++ b/run/test/specs/message_reaction.spec.ts @@ -45,9 +45,9 @@ async function sendEmojiReaction(platform: SupportedPlatformsType, testInfo: Tes }); await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { await Promise.all( - [alice1, bob1].map(async device => { - await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); - }) + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new EmojiReactsPill(device)) + ) ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/message_unsend.spec.ts b/run/test/specs/message_unsend.spec.ts index bd52940db..779d9ba40 100644 --- a/run/test/specs/message_unsend.spec.ts +++ b/run/test/specs/message_unsend.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; +import { DeletedMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -27,11 +27,7 @@ async function unsendMessage(platform: SupportedPlatformsType, testInfo: TestInf // send message from User A to User B const sentMessage = await alice1.sendMessage(testMessage); // await sleepFor(1000); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: sentMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); await alice1.longPressMessage(sentMessage); // Select Delete icon await alice1.clickOnByAccessibilityID('Delete message'); diff --git a/run/test/specs/message_video.spec.ts b/run/test/specs/message_video.spec.ts index f4d47b978..f2357122f 100644 --- a/run/test/specs/message_video.spec.ts +++ b/run/test/specs/message_video.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -37,17 +38,9 @@ async function sendVideoIos(platform: SupportedPlatformsType, testInfo: TestInfo // User B - Click on untrusted attachment message await bob1.trustAttachments(alice.userName); // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); const replyMessage = await bob1.replyToMessage(alice, testMessage); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } @@ -76,12 +69,7 @@ async function sendVideoAndroid(platform: SupportedPlatformsType, testInfo: Test await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await sleepFor(2000); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); - + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app and server await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index 9e9412a14..b786bc127 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -35,11 +36,6 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test await bob1.longPress('Voice message'); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); - - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); await closeApp(alice1, bob1); } diff --git a/run/test/specs/user_actions_block_conversation_options.spec.ts b/run/test/specs/user_actions_block_conversation_options.spec.ts index effb6dde6..0c44cd58d 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -8,7 +8,7 @@ import { BlockUserConfirmationModal, CloseSettings, } from './locators'; -import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { BlockedBanner, ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; @@ -81,10 +81,8 @@ async function blockUserInConversationSettings( // Send message from Blocked User await bob1.sendMessage(blockedMessage); await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: blockedMessage, - maxWait: 5000, + ...new MessageBody(alice1, blockedMessage).build(), + maxWait: 5_000, }); // Close app await closeApp(alice1, bob1); diff --git a/run/test/specs/user_actions_delete_contact_ucs.spec.ts b/run/test/specs/user_actions_delete_contact_ucs.spec.ts index 9071297ff..b6e34e506 100644 --- a/run/test/specs/user_actions_delete_contact_ucs.spec.ts +++ b/run/test/specs/user_actions_delete_contact_ucs.spec.ts @@ -8,6 +8,7 @@ import { ConversationSettings, DeleteContactConfirmButton, DeleteContactMenuItem, + MessageBody, } from './locators/conversation'; import { ConversationItem, MessageRequestsBanner } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; @@ -78,11 +79,7 @@ async function deleteContactCS(platform: SupportedPlatformsType, testInfo: TestI [alice1, alice2].map(async device => { await device.clickOnElementAll(new MessageRequestsBanner(device)); await device.clickOnByAccessibilityID('Message request'); - await device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: newMessage, - }); + await device.waitForTextElementToBePresent(new MessageBody(device, newMessage)); }) ); }); diff --git a/run/test/specs/user_actions_read_status.spec.ts b/run/test/specs/user_actions_read_status.spec.ts index eb7d96545..8ccc3d1cb 100644 --- a/run/test/specs/user_actions_read_status.spec.ts +++ b/run/test/specs/user_actions_read_status.spec.ts @@ -1,6 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; +import { MessageBody } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils/index'; @@ -31,11 +32,7 @@ async function readStatus(platform: SupportedPlatformsType, testInfo: TestInfo) await alice1.sendMessage(testMessage); await sleepFor(100); await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); // Check read status on device 1 await alice1.onAndroid().waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts index 5af17119e..b53fa5dd4 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -5,7 +5,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ImageName, ShareExtensionIcon } from './locators'; -import { MessageInput, SendButton } from './locators/conversation'; +import { MessageBody, MessageInput, SendButton } from './locators/conversation'; import { PhotoLibrary } from './locators/external'; import { Contact } from './locators/global'; import { open_Alice1_Bob1_friends } from './state_builder'; @@ -70,11 +70,7 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await bob1.trustAttachments(USERNAME.ALICE); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: testMessage, - }); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index d2d610f8e..537842469 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { BlockUser, BlockUserConfirmationModal } from './locators'; -import { BlockedBanner, ConversationSettings } from './locators/conversation'; +import { BlockedBanner, ConversationSettings, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType } from './utils/open_app'; @@ -50,10 +50,8 @@ async function unblockUser(platform: SupportedPlatformsType, testInfo: TestInfo) // Send message from Blocked User await bob1.sendMessage(blockedMessage); await alice1.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: blockedMessage, - maxWait: 5000, + ...new MessageBody(alice1, blockedMessage).build(), + maxWait: 5_000, }); // Now that user is blocked, unblock them await alice1.clickOnElementAll(new BlockedBanner(alice1)); diff --git a/run/test/specs/utils/create_contact.ts b/run/test/specs/utils/create_contact.ts index 16cdbeb51..eeba536f2 100644 --- a/run/test/specs/utils/create_contact.ts +++ b/run/test/specs/utils/create_contact.ts @@ -1,6 +1,7 @@ import { runOnlyOnIOS, sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; +import { MessageBody } from '../locators/conversation'; import { MessageRequestsBanner } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; @@ -28,11 +29,7 @@ export const newContact = async ( // TO DO - ADD BACK IN ONCE IOS AND ANDROID HAS FIXED THIS ISSUE // const messageRequestsAccepted = englishStrippedStr('messageRequestsAccepted').toString(); // await device1.onAndroid().waitForControlMessageToBePresent(messageRequestsAccepted); - await device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: replyMessage, - }); + await device1.waitForTextElementToBePresent(new MessageBody(device1, replyMessage)); console.info(`${sender.userName} and ${receiver.userName} are now contacts`); return { sender, receiver, device1, device2 }; }; diff --git a/run/test/specs/utils/create_group.ts b/run/test/specs/utils/create_group.ts index a2a29b575..3e0eae2f3 100644 --- a/run/test/specs/utils/create_group.ts +++ b/run/test/specs/utils/create_group.ts @@ -1,6 +1,7 @@ import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { Group, GROUPNAME, User } from '../../../types/testing'; +import { MessageBody } from '../locators/conversation'; import { Contact } from '../locators/global'; import { CreateGroupButton, GroupNameInput } from '../locators/groups'; import { ConversationItem, PlusButton } from '../locators/home'; @@ -84,45 +85,24 @@ export const createGroup = async ( // Send message from User A to group to verify all working await device1.sendMessage(aliceMessage); // Did the other devices receive alice's message? - await Promise.all([ - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: aliceMessage, - }), - ]); + await Promise.all( + [device2, device3].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, aliceMessage)) + ) + ); // Send message from User B to group await device2.sendMessage(bobMessage); - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: bobMessage, - }), - ]); + await Promise.all( + [device1, device3].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, bobMessage)) + ) + ); // Send message to User C to group await device3.sendMessage(charlieMessage); - await Promise.all([ - device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), - device2.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: charlieMessage, - }), - ]); + await Promise.all( + [device1, device2].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, charlieMessage)) + ) + ); return { userName, userOne, userTwo, userThree }; }; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index e8731faeb..d57164285 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -29,6 +29,7 @@ import { import { englishStrippedStr } from '../localizer/englishStrippedStr'; import { AttachmentsButton, + MessageBody, MessageInput, OutgoingMessageStatusSent, ScrollToBottomButton, @@ -479,10 +480,8 @@ export class DeviceWrapper { while (attempt < maxRetries && !success) { try { const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: textToLookFor, - maxWait: 1000, + ...new MessageBody(this, textToLookFor).build(), + maxWait: 1_000, }); if (!el) { throw new Error( @@ -769,12 +768,7 @@ export class DeviceWrapper { } public async findMessageWithBody(textToLookFor: string): Promise { - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message body', - text: textToLookFor, - }); - + await this.waitForTextElementToBePresent(new MessageBody(this, textToLookFor)); const message = await this.findMatchingTextAndAccessibilityId('Message body', textToLookFor); return message; } From bcb1c3dd5d9f1d1f854562363ca5ebfe2f683603 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 22 Aug 2025 13:59:33 +1000 Subject: [PATCH 29/88] fix: add visible check to verifyelementnotpresent --- run/types/DeviceWrapper.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index d57164285..643df2701 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -927,7 +927,7 @@ export class DeviceWrapper { } /** - * Ensures an element is not present on the screen at the end of the wait time. + * Ensures an element is not visible on the screen at the end of the wait time. * This allows any transitions to complete and tolerates some UI flakiness. * Unlike hasElementBeenDeleted, this doesn't require the element to exist first. * @@ -954,14 +954,24 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text: args.text }); - if (element) { - throw new Error( - `Element with ${description} is present after ${maxWait}ms when it should not be` - ); + if (element) { + // Elements can disappear in the GUI but still be present in the DOM + try { + const isVisible = await this.isVisible(element.ELEMENT); + if (isVisible) { + throw new Error( + `Element with ${description} is visible after ${maxWait}ms when it should not be` + ); + } + // Element exists but not visible - that's okay + this.log(`Element with ${description} exists but is not visible`); + } catch (e) { + // Stale element or other error - element is gone, that's okay + this.log(`Element with ${description} is not present (stale reference)`); + } + } else { + this.log(`Verified no element with ${description} is present`); } - - // Element not found - success! - this.log(`Verified no element with ${description} is present`); } /** @@ -1701,7 +1711,7 @@ export class DeviceWrapper { public async sendDocument() { if (this.isIOS()) { const formattedFileName = 'test_file, pdf'; - const testMessage = 'Testing-document-1'; + const testMessage = 'Testing documents'; copyFileToSimulator(this, testFile); await this.clickOnElementAll(new AttachmentsButton(this)); const keyboard = await this.isKeyboardVisible(); From cf201a55d48056717603dc4c6ec9a35c2f467ce5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 22 Aug 2025 13:59:51 +1000 Subject: [PATCH 30/88] fix more failing tests --- run/test/specs/disappearing_voice.spec.ts | 5 ++-- .../specs/group_message_long_text.spec.ts | 3 ++ run/test/specs/locators/settings.ts | 5 ++-- run/test/specs/message_requests_block.spec.ts | 29 +++++-------------- run/types/testing.ts | 1 + 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index 39ee0ffcf..f0e39163a 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { DISAPPEARING_TIMES } from '../../types/testing'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -36,7 +36,6 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes strategy: 'accessibility id', selector: 'Voice message', }); - await bob1.trustAttachments(USERNAME.ALICE); await Promise.all([ alice1.hasElementBeenDeleted({ strategy: 'accessibility id', @@ -46,7 +45,7 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes }), bob1.hasElementBeenDeleted({ strategy: 'accessibility id', - selector: 'Voice message', + selector: 'Untrusted attachment message', maxWait, preventEarlyDeletion: true, }), diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index 70da9c211..a18943372 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -12,6 +12,9 @@ bothPlatformsIt({ countOfDevicesNeeded: 3, testCb: sendLongMessageGroup, allureDescription: 'Verifies that a long message can be sent to a group', + allureLinks: { + android: 'SES-4337', + }, }); async function sendLongMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index f2f9bcf80..05c3628d5 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -182,8 +182,9 @@ export class ConversationsMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Conversations', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))', } as const; case 'ios': return { diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index 463d650bc..525dd27de 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -53,7 +53,7 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn strategy: 'accessibility id', selector: messageRequestsNonePending as AccessibilityId, }), - device3.hasElementBeenDeleted(new MessageRequestsBanner(device3)), + device3.verifyElementNotPresent({...new MessageRequestsBanner(device3).build(), maxWait: 5_000}), ]); const blockedMessage = `"${alice.userName} to ${bob.userName} - shouldn't get through"`; await device1.sendMessage(blockedMessage); @@ -63,26 +63,13 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn await sleepFor(5000); await device2.hasTextElementBeenDeleted('Message body', blockedMessage); // Check that user is on Blocked User list in Settings - - await Promise.all([ - device2.clickOnElementAll(new UserSettings(device2)), - device3.clickOnElementAll(new UserSettings(device3)), - ]); - // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it - await Promise.all([device2.scrollDown(), device3.scrollDown()]); - await Promise.all([ - device2.clickOnElementAll(new ConversationsMenuItem(device2)), - device3.clickOnElementAll(new ConversationsMenuItem(device3)), - ]); - await Promise.all([ - device2.clickOnElementAll(new BlockedContactsSettings(device2)), - device3.clickOnElementAll(new BlockedContactsSettings(device3)), - ]); - await Promise.all( - [device2, device3].map(device => - device.waitForTextElementToBePresent(new Contact(device, alice.userName)) - ) - ); +await Promise.all( + [device2, device3].map(async (device) => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new ConversationsMenuItem(device)); + await device.clickOnElementAll(new BlockedContactsSettings(device)); + await device.waitForTextElementToBePresent(new Contact(device, alice.userName)); + })); // Close app await closeApp(device1, device2, device3); } diff --git a/run/types/testing.ts b/run/types/testing.ts index 61bde4aca..7150b1886 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -155,6 +155,7 @@ export type XPath = export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' + | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' From ada09edeb029ff106a8ceb9cf20425b9489246e6 Mon Sep 17 00:00:00 2001 From: ThomasSession <171472362+ThomasSession@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:04:55 +0000 Subject: [PATCH 31/88] [Automated] Update translations from Crowdin --- run/localizer/constants.ts | 7 +- run/localizer/locales.ts | 482 ++++++++++++++++++++++++++++++++++++- 2 files changed, 475 insertions(+), 14 deletions(-) diff --git a/run/localizer/constants.ts b/run/localizer/constants.ts index 6fd2b45b0..dd2717db3 100644 --- a/run/localizer/constants.ts +++ b/run/localizer/constants.ts @@ -9,14 +9,19 @@ export enum LOCALE_DEFAULTS { token_name_short = 'SESH', usd_name_short = 'USD', app_pro = 'Session Pro', + session_foundation = 'Session Foundation', + pro = 'Pro', } export const rtlLocales = ['ar', 'fa', 'he', 'ps', 'ur']; -export const crowdinLocales = ['en'] as const; +export const crowdinLocales = [ + 'en', +] as const; export type CrowdinLocale = (typeof crowdinLocales)[number]; export function isCrowdinLocale(locale: string): locale is CrowdinLocale { return crowdinLocales.includes(locale as CrowdinLocale); } + diff --git a/run/localizer/locales.ts b/run/localizer/locales.ts index 94b497a5b..17397f859 100644 --- a/run/localizer/locales.ts +++ b/run/localizer/locales.ts @@ -41,7 +41,7 @@ type WithStoreVariant = {storevariant: string}; type WithMin = {min: string}; type WithMax = {max: string}; -export type TokenSimpleNoArgs = +export type TokenSimpleNoArgs = 'about' | 'accept' | 'accountIDCopy' | @@ -81,12 +81,15 @@ export type TokenSimpleNoArgs = 'appIconEnableIconAndName' | 'appIconSelect' | 'appIconSelectionTitle' | + 'appName' | 'appNameCalculator' | 'appNameMeetingSE' | 'appNameNews' | 'appNameNotes' | 'appNameStocks' | 'appNameWeather' | + 'appPro' | + 'appProBadge' | 'appearanceAutoDarkMode' | 'appearanceHideMenuBar' | 'appearanceLanguage' | @@ -198,6 +201,7 @@ export type TokenSimpleNoArgs = 'cameraGrantAccessDescription' | 'cameraGrantAccessQr' | 'cancel' | + 'cancelPlan' | 'change' | 'changePasswordFail' | 'changePasswordModalDescription' | @@ -286,6 +290,8 @@ export type TokenSimpleNoArgs = 'copy' | 'create' | 'creatingCall' | + 'currentPassword' | + 'currentPlan' | 'cut' | 'darkMode' | 'databaseErrorClearDataWarning' | @@ -449,6 +455,7 @@ export type TokenSimpleNoArgs = 'hideOthers' | 'image' | 'images' | + 'important' | 'incognitoKeyboard' | 'incognitoKeyboardDescription' | 'info' | @@ -472,6 +479,7 @@ export type TokenSimpleNoArgs = 'linkPreviewsSendModalDescription' | 'linkPreviewsTurnedOff' | 'linkPreviewsTurnedOffDescription' | + 'links' | 'loadAccount' | 'loadAccountProgressMessage' | 'loading' | @@ -484,7 +492,9 @@ export type TokenSimpleNoArgs = 'lockAppStatus' | 'lockAppUnlock' | 'lockAppUnlocked' | + 'logs' | 'manageMembers' | + 'managePro' | 'max' | 'media' | 'membersAddAccountIdOrOns' | @@ -534,7 +544,10 @@ export type TokenSimpleNoArgs = 'modalMessageCharacterDisplayTitle' | 'modalMessageCharacterTooLongTitle' | 'modalMessageTooLongTitle' | + 'networkName' | + 'newPassword' | 'next' | + 'nextSteps' | 'nicknameEnter' | 'nicknameErrorShorter' | 'nicknameRemove' | @@ -607,6 +620,7 @@ export type TokenSimpleNoArgs = 'open' | 'openSurvey' | 'other' | + 'oxenFoundation' | 'password' | 'passwordChange' | 'passwordChangeShortDescription' | @@ -630,8 +644,8 @@ export type TokenSimpleNoArgs = 'passwordSetShortDescription' | 'passwordStrengthCharLength' | 'passwordStrengthIncludeNumber' | - 'passwordStrengthIncludesLetter' | 'passwordStrengthIncludesLowercase' | + 'passwordStrengthIncludesSymbol' | 'passwordStrengthIncludesUppercase' | 'passwordStrengthIndicator' | 'passwordStrengthIndicatorDescription' | @@ -675,31 +689,68 @@ export type TokenSimpleNoArgs = 'pinConversation' | 'pinUnpin' | 'pinUnpinConversation' | + 'plusLoadsMore' | 'preferences' | 'preview' | 'previewNotification' | + 'pro' | 'proActivated' | + 'proAllSet' | 'proAlreadyPurchased' | 'proAnimatedDisplayPicture' | 'proAnimatedDisplayPictureCallToActionDescription' | 'proAnimatedDisplayPictureFeature' | 'proAnimatedDisplayPictureModalDescription' | + 'proAnimatedDisplayPictures' | + 'proAnimatedDisplayPicturesDescription' | 'proAnimatedDisplayPicturesNonProModalDescription' | 'proBadge' | + 'proBadgeVisible' | + 'proBadges' | + 'proBadgesDescription' | 'proCallToActionLongerMessages' | 'proCallToActionPinnedConversations' | 'proCallToActionPinnedConversationsMoreThan' | + 'proExpired' | + 'proExpiredDescription' | + 'proExpiringSoon' | + 'proFaq' | + 'proFaqDescription' | 'proFeatureListAnimatedDisplayPicture' | 'proFeatureListLargerGroups' | 'proFeatureListLoadsMore' | 'proFeatureListLongerMessages' | 'proFeatureListPinnedConversations' | + 'proFeatures' | 'proGroupActivated' | 'proGroupActivatedDescription' | + 'proImportantDescription' | 'proIncreasedAttachmentSizeFeature' | 'proIncreasedMessageLengthFeature' | + 'proLargerGroups' | + 'proLargerGroupsDescription' | + 'proLongerMessages' | + 'proLongerMessagesDescription' | 'proMessageInfoFeatures' | + 'proPlanNotFound' | + 'proPlanNotFoundDescription' | + 'proPlanRecover' | + 'proPlanRenew' | + 'proPlanRenewStart' | + 'proPlanRenewSupport' | + 'proPlanRestored' | + 'proPlanRestoredDescription' | + 'proRefundDescription' | + 'proRefundRequestSessionSupport' | + 'proRefunding' | + 'proRequestedRefund' | 'proSendMore' | + 'proSettings' | + 'proStats' | + 'proStatsTooltip' | + 'proSupportDescription' | + 'proUnlimitedPins' | + 'proUnlimitedPinsDescription' | 'proUserProfileModalCallToAction' | 'profile' | 'profileDisplayPicture' | @@ -750,7 +801,9 @@ export type TokenSimpleNoArgs = 'remove' | 'removePasswordFail' | 'removePasswordModalDescription' | + 'renew' | 'reply' | + 'requestRefund' | 'resend' | 'resolving' | 'restart' | @@ -784,6 +837,8 @@ export type TokenSimpleNoArgs = 'sessionAppearance' | 'sessionClearData' | 'sessionConversations' | + 'sessionDownloadUrl' | + 'sessionFoundation' | 'sessionHelp' | 'sessionInviteAFriend' | 'sessionMessageRequests' | @@ -799,6 +854,7 @@ export type TokenSimpleNoArgs = 'sessionNotifications' | 'sessionPermissions' | 'sessionPrivacy' | + 'sessionProBeta' | 'sessionRecoveryPassword' | 'sessionSettings' | 'set' | @@ -817,6 +873,7 @@ export type TokenSimpleNoArgs = 'showNoteToSelf' | 'showNoteToSelfDescription' | 'spellChecker' | + 'stakingRewardPool' | 'stickers' | 'strength' | 'supportDescription' | @@ -825,7 +882,10 @@ export type TokenSimpleNoArgs = 'theContinue' | 'theDefault' | 'theError' | + 'theReturn' | 'themePreview' | + 'tokenNameLong' | + 'tokenNameShort' | 'tooltipBlindedIdCommunities' | 'translate' | 'tray' | @@ -847,6 +907,8 @@ export type TokenSimpleNoArgs = 'updateGroupInformationDescription' | 'updateGroupInformationEnterShorterDescription' | 'updateNewVersion' | + 'updatePlan' | + 'updatePlanTwo' | 'updateProfileInformation' | 'updateProfileInformationDescription' | 'updateReleaseNotes' | @@ -858,6 +920,8 @@ export type TokenSimpleNoArgs = 'urlCopy' | 'urlOpen' | 'urlOpenBrowser' | + 'urlOpenDescriptionAlternative' | + 'usdNameShort' | 'useFastMode' | 'video' | 'videoErrorPlay' | @@ -1018,13 +1082,52 @@ export type TokensSimpleAndArgs = { notificationsMutedFor: WithTimeLarge, notificationsMutedForTime: WithDateTime, notificationsSystem: WithMessageCount & WithConversationCount, + onDevice: { device_type: string }, + onDeviceDescription: { device_type: string, platform_account: string }, onboardingBubbleCreatingAnAccountIsEasy: WithEmoji, onboardingBubbleWelcomeToSession: WithEmoji, + openStoreWebsite: { platform_store: string }, passwordErrorLength: WithMin & WithMax, + plusLoadsMoreDescription: WithIcon, + proAllSetDescription: WithDate, + proAutoRenewTime: WithTime, + proBadgesSent: WithCount, + proBilledAnnually: { price: string }, + proBilledMonthly: { price: string }, + proBilledQuarterly: { price: string }, + proDiscountTooltip: { percent: string }, + proExpiringSoonDescription: WithTime, + proGroupsUpgraded: WithCount, + proLongerMessagesSent: WithCount, + proPercentOff: { percent: string }, + proPinnedConversations: WithCount, + proPlanActivatedAuto: WithDate & { current_plan: string }, + proPlanActivatedAutoShort: WithDate & { current_plan: string }, + proPlanActivatedNotAuto: WithDate, + proPlanExpireDate: WithDate, + proPlanPlatformRefund: { platform_store: string, platform_account: string }, + proPlanPlatformRefundLong: { platform_store: string }, + proPlanRenewDesktop: { platform_store: string }, + proPlanRenewDesktopLinked: { platform_store: string }, + proPlanRenewDesktopStore: { platform_store: string, platform_account: string }, + proPlanSignUp: { platform_store: string, platform_account: string }, + proPriceOneMonth: { monthly_price: string }, + proPriceThreeMonths: { monthly_price: string }, + proPriceTwelveMonths: { monthly_price: string }, + proRefundNextSteps: { platform_account: string }, + proRefundRequestStorePolicies: { platform_account: string }, + proRefundSupport: { platform_account: string, platform_store: string }, + proRefundingDescription: { platform_account: string, platform_store: string }, + proTosPrivacy: WithIcon, + proUpdatePlanDescription: WithDate & { current_plan: string, selected_plan: string }, + proUpdatePlanExpireDescription: WithDate & { selected_plan: string }, + processingRefundRequest: { platform_account: string }, rateSessionModalDescription: WithStoreVariant, + refundPlanNonOriginatorApple: { platform_account: string }, remainingCharactersOverTooltip: WithCount, screenshotTaken: WithName, searchMatchesNoneSpecific: WithQuery, + sessionNetworkDataPrice: WithDateTime, sessionNetworkDescription: WithIcon, systemInformationDesktop: WithInformation, tooltipAccountIdVisible: WithName, @@ -1033,7 +1136,8 @@ export type TokensSimpleAndArgs = { updateVersion: WithVersion, updated: WithRelativeTime, urlOpenDescription: WithUrl, - sessionNetworkDataPrice: WithDateTime + viaStoreWebsite: { platform_store: string }, + viaStoreWebsiteDescription: { platform_account: string, platform_store: string } }; export type TokensPluralAndArgs = { @@ -1065,7 +1169,7 @@ export type TokensPluralAndArgs = { searchMatches: WithFoundCount & WithCount }; -export type TokenSimpleWithArgs = +export type TokenSimpleWithArgs = 'accountIdShare' | 'adminMorePromotedToAdmin' | 'adminPromoteDescription' | @@ -1209,13 +1313,52 @@ export type TokenSimpleWithArgs = 'notificationsMutedFor' | 'notificationsMutedForTime' | 'notificationsSystem' | + 'onDevice' | + 'onDeviceDescription' | 'onboardingBubbleCreatingAnAccountIsEasy' | 'onboardingBubbleWelcomeToSession' | + 'openStoreWebsite' | 'passwordErrorLength' | + 'plusLoadsMoreDescription' | + 'proAllSetDescription' | + 'proAutoRenewTime' | + 'proBadgesSent' | + 'proBilledAnnually' | + 'proBilledMonthly' | + 'proBilledQuarterly' | + 'proDiscountTooltip' | + 'proExpiringSoonDescription' | + 'proGroupsUpgraded' | + 'proLongerMessagesSent' | + 'proPercentOff' | + 'proPinnedConversations' | + 'proPlanActivatedAuto' | + 'proPlanActivatedAutoShort' | + 'proPlanActivatedNotAuto' | + 'proPlanExpireDate' | + 'proPlanPlatformRefund' | + 'proPlanPlatformRefundLong' | + 'proPlanRenewDesktop' | + 'proPlanRenewDesktopLinked' | + 'proPlanRenewDesktopStore' | + 'proPlanSignUp' | + 'proPriceOneMonth' | + 'proPriceThreeMonths' | + 'proPriceTwelveMonths' | + 'proRefundNextSteps' | + 'proRefundRequestStorePolicies' | + 'proRefundSupport' | + 'proRefundingDescription' | + 'proTosPrivacy' | + 'proUpdatePlanDescription' | + 'proUpdatePlanExpireDescription' | + 'processingRefundRequest' | 'rateSessionModalDescription' | + 'refundPlanNonOriginatorApple' | 'remainingCharactersOverTooltip' | 'screenshotTaken' | 'searchMatchesNoneSpecific' | + 'sessionNetworkDataPrice' | 'sessionNetworkDescription' | 'systemInformationDesktop' | 'tooltipAccountIdVisible' | @@ -1224,9 +1367,10 @@ export type TokenSimpleWithArgs = 'updateVersion' | 'updated' | 'urlOpenDescription' | - 'sessionNetworkDataPrice' + 'viaStoreWebsite' | + 'viaStoreWebsiteDescription' -export type TokenPluralWithArgs = +export type TokenPluralWithArgs = 'adminSendingPromotion' | 'clearDataErrorDescription' | 'deleteMessage' | @@ -1375,6 +1519,9 @@ export const simpleDictionaryNoArgs: Record< appIconSelectionTitle: { en: "Icon", }, + appName: { + en: "Session", + }, appNameCalculator: { en: "Calculator", }, @@ -1393,6 +1540,12 @@ export const simpleDictionaryNoArgs: Record< appNameWeather: { en: "Weather", }, + appPro: { + en: "Session Pro", + }, + appProBadge: { + en: "Session Pro Badge", + }, appearanceAutoDarkMode: { en: "Auto Dark Mode", }, @@ -1700,7 +1853,7 @@ export const simpleDictionaryNoArgs: Record< en: "Voice and Video Calls (Beta)", }, callsVoiceAndVideoModalDescription: { - en: "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.", + en: "Your IP is visible to your call partner and a Session Foundation server while using beta calls.", }, callsVoiceAndVideoToggleDescription: { en: "Enables voice and video calls to and from other users.", @@ -1726,6 +1879,9 @@ export const simpleDictionaryNoArgs: Record< cancel: { en: "Cancel", }, + cancelPlan: { + en: "Cancel Plan", + }, change: { en: "Change", }, @@ -1990,6 +2146,12 @@ export const simpleDictionaryNoArgs: Record< creatingCall: { en: "Creating Call", }, + currentPassword: { + en: "Current Password", + }, + currentPlan: { + en: "Current Plan", + }, cut: { en: "Cut", }, @@ -2479,6 +2641,9 @@ export const simpleDictionaryNoArgs: Record< images: { en: "images", }, + important: { + en: "Important", + }, incognitoKeyboard: { en: "Incognito Keyboard", }, @@ -2548,6 +2713,9 @@ export const simpleDictionaryNoArgs: Record< linkPreviewsTurnedOffDescription: { en: "Session must contact linked websites to generate previews of links you send and receive.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{url}", }, - sessionNetworkDataPrice: { - en: "Price data powered by CoinGecko
Accurate at {date_time}", + viaStoreWebsite: { + en: "Via the {platform_store} website", + }, + viaStoreWebsiteDescription: { + en: "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website.", }, } as const; From 6da72d4415b85118b3dd177e55ed05910a3f0f99 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 1 Sep 2025 11:36:25 +1000 Subject: [PATCH 32/88] feat: app review tests now work on ios too --- run/test/specs/locators/home.ts | 25 +++++++-- run/test/specs/locators/settings.ts | 35 +++++++++++++ run/test/specs/message_requests_block.spec.ts | 20 ++++--- run/test/specs/review_negative.spec.ts | 4 +- run/test/specs/review_positive.spec.ts | 4 +- run/test/specs/review_triggers.spec.ts | 11 ++-- run/types/DeviceWrapper.ts | 52 +++++++------------ run/types/testing.ts | 8 +++ 8 files changed, 106 insertions(+), 53 deletions(-) diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index 9829b0b2e..b43962511 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -100,7 +100,10 @@ export class ReviewPromptItsGreatButton extends LocatorsInterface { selector: 'enjoy-session-positive-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'enjoy-session-positive-button', + }; } } } @@ -114,7 +117,10 @@ export class ReviewPromptNeedsWorkButton extends LocatorsInterface { selector: 'enjoy-session-negative-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'enjoy-session-negative-button', + }; } } } @@ -128,7 +134,10 @@ export class ReviewPromptRateAppButton extends LocatorsInterface { selector: 'rate-app-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'rate-app-button', + }; } } } @@ -142,7 +151,10 @@ export class ReviewPromptNotNowButton extends LocatorsInterface { selector: 'not-now-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'not-now-button', + }; } } } @@ -156,7 +168,10 @@ export class ReviewPromptOpenSurveyButton extends LocatorsInterface { selector: 'open-survey-button', }; case 'ios': - throw new Error('Not implemented'); + return { + strategy: 'accessibility id', + selector: 'open-survey-button', + }; } } } diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 05c3628d5..319232323 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -213,6 +213,23 @@ export class AppearanceMenuItem extends LocatorsInterface { } } +export class ClassicLightThemeOption extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger.qa:id/theme_option_classic_light', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Classic Light', + } as const; + } + } +} + export class SelectAppIcon extends LocatorsInterface { public build() { switch (this.platform) { @@ -311,3 +328,21 @@ export class PathMenuItem extends LocatorsInterface { } } } + +export class VersionNumber extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', + } as const; + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[contains(@name, "Version")]`, + } as const; + } + } +} diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index 525dd27de..cb09d1113 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -53,7 +53,10 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn strategy: 'accessibility id', selector: messageRequestsNonePending as AccessibilityId, }), - device3.verifyElementNotPresent({...new MessageRequestsBanner(device3).build(), maxWait: 5_000}), + device3.verifyElementNotPresent({ + ...new MessageRequestsBanner(device3).build(), + maxWait: 5_000, + }), ]); const blockedMessage = `"${alice.userName} to ${bob.userName} - shouldn't get through"`; await device1.sendMessage(blockedMessage); @@ -63,13 +66,14 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn await sleepFor(5000); await device2.hasTextElementBeenDeleted('Message body', blockedMessage); // Check that user is on Blocked User list in Settings -await Promise.all( - [device2, device3].map(async (device) => { - await device.clickOnElementAll(new UserSettings(device)); - await device.clickOnElementAll(new ConversationsMenuItem(device)); - await device.clickOnElementAll(new BlockedContactsSettings(device)); - await device.waitForTextElementToBePresent(new Contact(device, alice.userName)); - })); + await Promise.all( + [device2, device3].map(async device => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new ConversationsMenuItem(device)); + await device.clickOnElementAll(new BlockedContactsSettings(device)); + await device.waitForTextElementToBePresent(new Contact(device, alice.userName)); + }) + ); // Close app await closeApp(device1, device2, device3); } diff --git a/run/test/specs/review_negative.spec.ts b/run/test/specs/review_negative.spec.ts index c5bd1439f..ebf636b8b 100644 --- a/run/test/specs/review_negative.spec.ts +++ b/run/test/specs/review_negative.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ReviewPromptNeedsWorkButton, @@ -14,7 +14,7 @@ import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { assertUrlIsReachable } from './utils/utilities'; -androidIt({ +bothPlatformsIt({ title: 'Review prompt negative flow', risk: 'high', countOfDevicesNeeded: 1, diff --git a/run/test/specs/review_positive.spec.ts b/run/test/specs/review_positive.spec.ts index 9ed8c8c40..7432b7439 100644 --- a/run/test/specs/review_positive.spec.ts +++ b/run/test/specs/review_positive.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ReviewPromptItsGreatButton, @@ -13,7 +13,7 @@ import { PathMenuItem, UserSettings } from './locators/settings'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -androidIt({ +bothPlatformsIt({ title: 'Review prompt positive flow', risk: 'high', countOfDevicesNeeded: 1, diff --git a/run/test/specs/review_triggers.spec.ts b/run/test/specs/review_triggers.spec.ts index ac2213856..cf92d35ab 100644 --- a/run/test/specs/review_triggers.spec.ts +++ b/run/test/specs/review_triggers.spec.ts @@ -3,10 +3,11 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { AppearanceMenuItem, + ClassicLightThemeOption, DonationsMenuItem, PathMenuItem, UserSettings, @@ -22,6 +23,7 @@ const reviewTriggers = [ testStepName: 'Open Donations menu item', trigger: async (device: DeviceWrapper) => { await device.clickOnElementAll(new DonationsMenuItem(device)); + await device.clickOnByAccessibilityID('Copy URL'); // Copy URL dismisses the modal on both platforms }, }, { @@ -30,6 +32,7 @@ const reviewTriggers = [ testStepName: TestSteps.OPEN.PATH, trigger: async (device: DeviceWrapper) => { await device.clickOnElementAll(new PathMenuItem(device)); + await device.back(); }, }, { @@ -39,13 +42,14 @@ const reviewTriggers = [ trigger: async (device: DeviceWrapper) => { await device.scrollDown(); await device.clickOnElementAll(new AppearanceMenuItem(device)); - await device.clickOnElementById('network.loki.messenger.qa:id/theme_option_classic_light'); + await device.clickOnElementAll(new ClassicLightThemeOption(device)); + await device.back(); }, }, ]; for (const { titleSnippet, descriptionSnippet, testStepName, trigger } of reviewTriggers) { - androidIt({ + bothPlatformsIt({ title: `Review prompt ${titleSnippet} trigger`, risk: 'high', countOfDevicesNeeded: 1, @@ -67,7 +71,6 @@ for (const { titleSnippet, descriptionSnippet, testStepName, trigger } of review await device.clickOnElementAll(new UserSettings(device)); await trigger(device); await device.back(); - await device.back(); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('App Review'), async () => { await device.checkModalStrings( diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 643df2701..5cc5dca82 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -43,6 +43,7 @@ import { SaveProfilePictureButton, UserAvatar, UserSettings, + VersionNumber, } from '../test/specs/locators/settings'; import { EnterAccountID, @@ -954,23 +955,23 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text: args.text }); - if (element) { - // Elements can disappear in the GUI but still be present in the DOM - try { - const isVisible = await this.isVisible(element.ELEMENT); - if (isVisible) { - throw new Error( - `Element with ${description} is visible after ${maxWait}ms when it should not be` - ); - } - // Element exists but not visible - that's okay - this.log(`Element with ${description} exists but is not visible`); - } catch (e) { - // Stale element or other error - element is gone, that's okay - this.log(`Element with ${description} is not present (stale reference)`); + if (element) { + // Elements can disappear in the GUI but still be present in the DOM + try { + const isVisible = await this.isVisible(element.ELEMENT); + if (isVisible) { + throw new Error( + `Element with ${description} is visible after ${maxWait}ms when it should not be` + ); } - } else { - this.log(`Verified no element with ${description} is present`); + // Element exists but not visible - that's okay + this.log(`Element with ${description} exists but is not visible`); + } catch (e) { + // Stale element or other error - element is gone, that's okay + this.log(`Element with ${description} is not present (stale reference)`); + } + } else { + this.log(`Verified no element with ${description} is present`); } } @@ -2244,25 +2245,12 @@ export class DeviceWrapper { } public async getVersionNumber() { - if (this.isIOS()) { - throw new Error('getVersionNumber not implemented on iOS yet'); - } - await this.clickOnElementAll(new UserSettings(this)); - // Find the element using UiScrollable - const versionElement = await this.waitForTextElementToBePresent({ - strategy: '-android uiautomator', - selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', - }); - + const versionElement = await this.waitForTextElementToBePresent(new VersionNumber(this)); // Get the full text from the element - const versionText = await this.getAttribute('text', versionElement.ELEMENT); - // versionText will be something like "Version 1.27.0 (4175 - ac77d8) - Mainnet" - - // Extract just the version number + const versionText = await this.getTextFromElement(versionElement); + // Extract just the version number (e.g. "1.27.0") const match = versionText?.match(/(\d+\.\d+\.\d+)/); - // match[1] will be "1.27.0" if (!match) { throw new Error(`Could not extract version from: ${versionText}`); diff --git a/run/types/testing.ts b/run/types/testing.ts index 7150b1886..2d14f65c9 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -145,6 +145,7 @@ export type XPath = | `//XCUIElementTypeStaticText[@name="Paste"]` | `//XCUIElementTypeStaticText[@name="Videos"]` | `//XCUIElementTypeStaticText[contains(@name, '00:')]` + | `//XCUIElementTypeStaticText[contains(@name, "Version")]` | `//XCUIElementTypeSwitch[@name="Read Receipts, Send read receipts in one-to-one chats."]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[2]/android.widget.Button[1]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.LinearLayout` @@ -197,6 +198,7 @@ export type AccessibilityId = | 'Call' | 'Call button' | 'Cancel' + | 'Classic Light' | 'Clear' | 'Clear all' | 'Close' @@ -220,6 +222,7 @@ export type AccessibilityId = | 'Conversations' | 'Copy' | 'Copy button' + | 'Copy URL' | 'Create account button' | 'Create group' | 'Decline message request' @@ -259,6 +262,8 @@ export type AccessibilityId = | 'Empty state label' | 'Enable' | 'End call button' + | 'enjoy-session-negative-button' + | 'enjoy-session-positive-button' | 'Enter Community URL' | 'Enter display name' | 'Error message' @@ -323,12 +328,14 @@ export type AccessibilityId = | 'Nickname' | 'No' | 'No pending message requests' + | 'not-now-button' | 'Note to Self' | 'Notifications' | 'Off' | 'OK_BUTTON' | 'OK' | 'Okay' + | 'open-survey-button' | 'Open' | 'Open URL' | 'Path' @@ -337,6 +344,7 @@ export type AccessibilityId = | 'Pin' | 'Please enter a shorter group name' | 'Privacy Policy' + | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' | 'Recovery password' From 872e7af690490e1a84e89e7fed0ed6bb13b07f18 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 1 Sep 2025 11:45:38 +1000 Subject: [PATCH 33/88] fix: don't store version as devicewrapper property --- run/types/DeviceWrapper.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5cc5dca82..ad94a49bf 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -89,7 +89,6 @@ export class DeviceWrapper { private readonly device: AndroidUiautomator2Driver | XCUITestDriver; public readonly udid: string; private deviceIdentity: string = ''; - private version: string | null = null; constructor(device: AndroidUiautomator2Driver | XCUITestDriver, udid: string) { this.device = device; @@ -2245,6 +2244,7 @@ export class DeviceWrapper { } public async getVersionNumber() { + // NOTE if this becomes necessary for more tests, consider adding a property/caching to the DeviceWrapper await this.clickOnElementAll(new UserSettings(this)); const versionElement = await this.waitForTextElementToBePresent(new VersionNumber(this)); // Get the full text from the element @@ -2256,8 +2256,7 @@ export class DeviceWrapper { throw new Error(`Could not extract version from: ${versionText}`); } - this.version = match[1]; - return this.version; + return match[1]; } private getUdid() { From cb6b6c176761bd0d2701fb6a9a1c1260e00de7b2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 1 Sep 2025 11:58:06 +1000 Subject: [PATCH 34/88] fix: correct locator for copy url --- run/test/specs/locators/global.ts | 17 +++++++++++++++++ run/test/specs/review_triggers.spec.ts | 3 ++- run/types/testing.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index c5c658d95..dc4b1e300 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -142,3 +142,20 @@ export class AccountIDDisplay extends LocatorsInterface { } } } + +export class CopyURLButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Copy URL', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Copy URL', + } as const; + } + } +} diff --git a/run/test/specs/review_triggers.spec.ts b/run/test/specs/review_triggers.spec.ts index cf92d35ab..b19e91022 100644 --- a/run/test/specs/review_triggers.spec.ts +++ b/run/test/specs/review_triggers.spec.ts @@ -5,6 +5,7 @@ import { TestSteps } from '../../types/allure'; import { DeviceWrapper } from '../../types/DeviceWrapper'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { CopyURLButton } from './locators/global'; import { AppearanceMenuItem, ClassicLightThemeOption, @@ -23,7 +24,7 @@ const reviewTriggers = [ testStepName: 'Open Donations menu item', trigger: async (device: DeviceWrapper) => { await device.clickOnElementAll(new DonationsMenuItem(device)); - await device.clickOnByAccessibilityID('Copy URL'); // Copy URL dismisses the modal on both platforms + await device.clickOnElementAll(new CopyURLButton(device)); // Copy URL dismisses the modal on both platforms }, }, { diff --git a/run/types/testing.ts b/run/types/testing.ts index 2d14f65c9..9842cf931 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -445,6 +445,7 @@ export type Id = | 'Conversation header name' | 'Conversations' | 'Copy button' + | 'Copy URL' | 'Create account button' | 'Create group' | 'delete-contact-confirm-button' From e36e317093ad1da58ae937672855182d4d796e4a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 2 Sep 2025 09:20:10 +1000 Subject: [PATCH 35/88] fix group document testMessage --- run/test/specs/group_message_document.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/group_message_document.spec.ts b/run/test/specs/group_message_document.spec.ts index 930302f78..fbec7d80b 100644 --- a/run/test/specs/group_message_document.spec.ts +++ b/run/test/specs/group_message_document.spec.ts @@ -29,7 +29,7 @@ async function sendDocumentGroupiOS(platform: SupportedPlatformsType, testInfo: focusGroupConvo: true, testInfo, }); - const testMessage = 'Testing-document'; + const testMessage = 'Testing documents'; const replyMessage = `Replying to document from ${alice.userName}`; await alice1.sendDocument(); From 89536c2af5874f69a99b35f93f9e6f4f47ec256f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 2 Sep 2025 09:20:26 +1000 Subject: [PATCH 36/88] chore: bump GIF DM timers and wait intervals --- run/test/specs/disappearing_gif.spec.ts | 2 +- run/test/specs/group_disappearing_messages_gif.spec.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index f4eefa5af..19baefe12 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -21,7 +21,7 @@ bothPlatformsIt({ // The timing with 30 seconds was a bit tight in terms of the attachment downloading and becoming visible const time = DISAPPEARING_TIMES.ONE_MINUTE; const initialMaxWait = 15_000; // GIFs could be large so give them a bit more time to be found -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 70s plus buffer const timerType = 'Disappear after send option'; async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index a4f8cfa70..e2c25ad9d 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -18,10 +18,11 @@ bothPlatformsIt({ allureDescription: 'Verifies that a GIF disappears as expected in a group conversation', }); -const time = DISAPPEARING_TIMES.THIRTY_SECONDS; +// The timing with 30 seconds was a bit tight in terms of the attachment downloading and becoming visible +const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const initialMaxWait = 15_000; // Downloading the attachment can take a while -const maxWait = 35_000; // 30s plus buffer +const maxWait = 70_000; // 70s plus buffer async function disappearingGifMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Disappear after sent test'; From 56475bb0a152bcf31f4c4db66ae2488db243b630 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 2 Sep 2025 11:11:14 +1000 Subject: [PATCH 37/88] refactor: delete for everyone checks on home screen too --- .../linked_device_unsend_message.spec.ts | 100 ++++++++++++------ run/test/specs/locators/home.ts | 28 +++++ run/types/allure.ts | 3 + run/types/testing.ts | 2 + 4 files changed, 102 insertions(+), 31 deletions(-) diff --git a/run/test/specs/linked_device_unsend_message.spec.ts b/run/test/specs/linked_device_unsend_message.spec.ts index cf57d807e..a469f8e9b 100644 --- a/run/test/specs/linked_device_unsend_message.spec.ts +++ b/run/test/specs/linked_device_unsend_message.spec.ts @@ -1,10 +1,11 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; -import { DeletedMessage } from './locators/conversation'; -import { ConversationItem } from './locators/home'; +import { DeletedMessage, MessageBody } from './locators/conversation'; +import { ConversationItem, MessageInConversation } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -13,41 +14,78 @@ bothPlatformsIt({ risk: 'medium', testCb: unSendMessageLinkedDevice, countOfDevicesNeeded: 3, + allureSuites: { + parent: 'User Actions', + suite: 'Delete Message', + }, + allureDescription: `Verifies that 'Delete for everyone' in a 1-1 deletes the message in the conversation view and on the home screen for both parties and a linked device.`, }); async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testInfo: TestInfo) { + const firstMessage = 'Hello'; + const secondMessage = 'Howdy'; const { devices: { alice1, alice2, bob1 }, - prebuilt: { bob }, - } = await open_Alice2_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); - - // Send message from user a to user b - const sentMessage = await alice1.sendMessage('Howdy'); + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice2_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + await alice1.sendMessage(firstMessage); + await alice1.sendMessage(secondMessage); + }); // Check message came through on linked device(3) // Enter conversation with user B on device 3 - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2)); - await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); - // Find message - await alice2.findMessageWithBody(sentMessage); - // Select message on device 1, long press - await alice1.longPressMessage(sentMessage); - // Select delete - await alice1.clickOnByAccessibilityID('Delete message'); - await alice1.checkModalStrings( - englishStrippedStr('deleteMessage').withArgs({ count: 1 }).toString(), - englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() - ); - // Select delete for everyone - await alice1.clickOnElementAll(new DeleteMessageForEveryone(alice1)); - await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); - await Promise.all( - [alice1, bob1, alice2].map(device => - device.waitForTextElementToBePresent({ - ...new DeletedMessage(device).build(), - maxWait: 5000, + await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { + await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); + // Find message + await Promise.all( + [bob1, alice2].map(async device => { + await device.findMessageWithBody(firstMessage); + await device.findMessageWithBody(secondMessage); + }) + ); + }); + await test.step(TestSteps.USER_ACTIONS.DELETE_FOR_EVERYONE, async () => { + // Select message on device 1, long press + await alice1.longPressMessage(secondMessage); + await alice1.clickOnByAccessibilityID('Delete message'); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Delete message'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('deleteMessage').withArgs({ count: 1 }).toString(), + englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() + ); + }); + // Select delete for everyone + await alice1.clickOnElementAll(new DeleteMessageForEveryone(alice1)); + await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DELETED('conversation view'), async () => { + await Promise.all( + [alice1, bob1, alice2].map(async device => { + await device.waitForTextElementToBePresent(new MessageBody(device, firstMessage)); + await device.waitForTextElementToBePresent({ + ...new DeletedMessage(device).build(), + maxWait: 10_000, + }); + await device.back(); }) - ) - ); + ); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DELETED('home screen'), async () => { + await Promise.all( + [alice1, alice2].map(device => + device.waitForTextElementToBePresent( + new MessageInConversation(device, bob.userName, firstMessage) + ) + ) + ); + await bob1.waitForTextElementToBePresent( + new MessageInConversation(bob1, alice.userName, firstMessage) + ); + }); // Close app - await closeApp(alice1, bob1, alice2); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, alice2); + }); } diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index b43962511..56c4c492a 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -52,6 +52,34 @@ export class ConversationItem extends LocatorsInterface { } } +// For identifying a conversation with a specific last message in it +export class MessageInConversation extends LocatorsInterface { + public conversationName: string; + public messageText: string; + + constructor(device: DeviceWrapper, conversationName: string, messageText: string) { + super(device); + this.conversationName = conversationName; + this.messageText = messageText; + } + + public build() { + switch (this.platform) { + case 'ios': + return { + strategy: 'xpath', + selector: `//XCUIElementTypeCell[@name="Conversation list item" and @label="${this.conversationName}"]//XCUIElementTypeStaticText[@name="${this.messageText}"]`, + } as const; + + case 'android': + return { + strategy: 'xpath', + selector: `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${this.conversationName}"]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/snippetTextView" and @text="${this.messageText}"]`, + } as const; + } + } +} + export class PlusButton extends LocatorsInterface { public build() { return { diff --git a/run/types/allure.ts b/run/types/allure.ts index 5d5157914..adc7c7bb0 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -34,6 +34,7 @@ export type AllureSuiteConfig = | 'Change Profile Picture' | 'Delete Contact' | 'Delete Conversation' + | 'Delete Message' | 'Hide Note to Self' | 'Set Nickname' | 'Share to Session'; @@ -84,6 +85,7 @@ export const TestSteps = { USER_ACTIONS: { CHANGE_PROFILE_PICTURE: 'Change profile picture', APP_DISGUISE: 'Set App Disguise', + DELETE_FOR_EVERYONE: 'Delete for everyone', }, // Disappearing Messages DISAPPEARING_MESSAGES: { @@ -101,6 +103,7 @@ export const TestSteps = { SPECIFIC_MODAL: (modalDesc: string) => `Verify ${modalDesc} modal strings`, MESSAGE_RECEIVED: 'Verify message has been received', MESSAGE_DISAPPEARED: 'Verify message disappeared', + MESSAGE_DELETED: (context: string) => `Verify message deleted in/on ${context}`, CALLING: 'Verify call has been started', CALL_SUCCESSFUL: 'Verify call has been put through successfully', MISSED_CALL: 'Verify missed call', diff --git a/run/types/testing.ts b/run/types/testing.ts index 9842cf931..949e3f0ab 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -134,11 +134,13 @@ export type XPath = | `//*[./*[@name='${DISAPPEARING_TIMES}']]/*[2]` | `//*[@resource-id='network.loki.messenger.qa:id/callTitle' and contains(@text, ':')]` | `//*[starts-with(@content-desc, "Photo taken on")]` + | `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${string}"]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/snippetTextView" and @text="${string}"]` | `//android.widget.TextView[@text="${string}"]` | `//XCUIElementTypeAlert//*//XCUIElementTypeButton` | `//XCUIElementTypeButton[@name="Continue"]` | `//XCUIElementTypeButton[@name="Settings"]` | `//XCUIElementTypeCell[@name="${string}"]` + | `//XCUIElementTypeCell[@name="Conversation list item" and @label="${string}"]//XCUIElementTypeStaticText[@name="${string}"]` | `//XCUIElementTypeCell[@name="Session"]` | `//XCUIElementTypeImage` | `//XCUIElementTypeOther[contains(@name, "Hey,")][1]` From 706c32da2dd02c0a8c7072efc5c6db28d106f080 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 2 Sep 2025 11:22:59 +1000 Subject: [PATCH 38/88] chore: tidy up unsent message test --- .../linked_device_unsend_message.spec.ts | 34 ++++++++++++------- run/test/specs/locators/home.ts | 4 +-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/run/test/specs/linked_device_unsend_message.spec.ts b/run/test/specs/linked_device_unsend_message.spec.ts index a469f8e9b..3e43eb38a 100644 --- a/run/test/specs/linked_device_unsend_message.spec.ts +++ b/run/test/specs/linked_device_unsend_message.spec.ts @@ -5,7 +5,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DeleteMessageConfirmationModal, DeleteMessageForEveryone } from './locators'; import { DeletedMessage, MessageBody } from './locators/conversation'; -import { ConversationItem, MessageInConversation } from './locators/home'; +import { ConversationItem, MessageSnippet } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -22,20 +22,23 @@ bothPlatformsIt({ }); async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testInfo: TestInfo) { + // we send two messages to make sure deletion works OK const firstMessage = 'Hello'; const secondMessage = 'Howdy'; + const { devices: { alice1, alice2, bob1 }, prebuilt: { alice, bob }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return open_Alice2_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { await alice1.sendMessage(firstMessage); await alice1.sendMessage(secondMessage); }); - // Check message came through on linked device(3) - // Enter conversation with user B on device 3 + + // Check message came through on alice2 and bob1 await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); // Find message @@ -46,8 +49,9 @@ async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testI }) ); }); + + // alice1 deletes message for everyone await test.step(TestSteps.USER_ACTIONS.DELETE_FOR_EVERYONE, async () => { - // Select message on device 1, long press await alice1.longPressMessage(secondMessage); await alice1.clickOnByAccessibilityID('Delete message'); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Delete message'), async () => { @@ -56,14 +60,16 @@ async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testI englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() ); }); - // Select delete for everyone await alice1.clickOnElementAll(new DeleteMessageForEveryone(alice1)); await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); }); + + // check the first message is still there, the second message deleted and replaced with deleted message await test.step(TestSteps.VERIFY.MESSAGE_DELETED('conversation view'), async () => { await Promise.all( [alice1, bob1, alice2].map(async device => { await device.waitForTextElementToBePresent(new MessageBody(device, firstMessage)); + await device.verifyElementNotPresent(new MessageBody(device, secondMessage)); await device.waitForTextElementToBePresent({ ...new DeletedMessage(device).build(), maxWait: 10_000, @@ -72,19 +78,23 @@ async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testI }) ); }); + + // check home screen snippet now shows first message again await test.step(TestSteps.VERIFY.MESSAGE_DELETED('home screen'), async () => { await Promise.all( [alice1, alice2].map(device => - device.waitForTextElementToBePresent( - new MessageInConversation(device, bob.userName, firstMessage) - ) + device.waitForTextElementToBePresent({ + ...new MessageSnippet(device, bob.userName, firstMessage).build(), + maxWait: 5_000, + }) ) ); - await bob1.waitForTextElementToBePresent( - new MessageInConversation(bob1, alice.userName, firstMessage) - ); + await bob1.waitForTextElementToBePresent({ + ...new MessageSnippet(bob1, alice.userName, firstMessage).build(), + maxWait: 5_000, + }); }); - // Close app + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1, alice2); }); diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index 56c4c492a..bcbe980e4 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -53,7 +53,7 @@ export class ConversationItem extends LocatorsInterface { } // For identifying a conversation with a specific last message in it -export class MessageInConversation extends LocatorsInterface { +export class MessageSnippet extends LocatorsInterface { public conversationName: string; public messageText: string; @@ -67,7 +67,7 @@ export class MessageInConversation extends LocatorsInterface { switch (this.platform) { case 'ios': return { - strategy: 'xpath', + strategy: 'xpath', // For nested elements like this xpath is unfortunately the best choice selector: `//XCUIElementTypeCell[@name="Conversation list item" and @label="${this.conversationName}"]//XCUIElementTypeStaticText[@name="${this.messageText}"]`, } as const; From 55c99545f2c8374e98dd84731d22853728817106 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 2 Sep 2025 15:27:04 +1000 Subject: [PATCH 39/88] fix: review prompt url prints iOS not ios --- run/test/specs/review_negative.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run/test/specs/review_negative.spec.ts b/run/test/specs/review_negative.spec.ts index ebf636b8b..63c6c88fc 100644 --- a/run/test/specs/review_negative.spec.ts +++ b/run/test/specs/review_negative.spec.ts @@ -35,7 +35,8 @@ async function reviewPromptNegative(platform: SupportedPlatformsType, testInfo: }); const version = await device.getVersionNumber(); - const url = `https://getsession.org/feedback?platform=${platform}&version=${version}`; + const platformParam = platform === 'ios' ? 'iOS' : 'android'; // we call it ios but the app prints iOS + const url = `https://getsession.org/feedback?platform=${platformParam}&version=${version}`; await test.step(TestSteps.OPEN.PATH, async () => { await device.clickOnElementAll(new PathMenuItem(device)); From 2ec35bec74d1532449ab7459b75dff0510130b30 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 3 Sep 2025 15:59:27 +1000 Subject: [PATCH 40/88] fix: update locator for blocked contacts --- run/test/specs/locators/index.ts | 5 ++--- run/types/testing.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index af4b5888a..c8b8767db 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -398,9 +398,8 @@ export class BlockedContactsSettings extends LocatorsInterface { switch (this.platform) { case 'android': return { - // Temporary fix until there's a unique ID - strategy: '-android uiautomator', - selector: `new UiSelector().text("View and manage blocked contacts.")`, + strategy: 'accessibility id', + selector: 'qa-blocked-contacts-settings-item' }; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index 949e3f0ab..a1f73650f 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -346,6 +346,7 @@ export type AccessibilityId = | 'Pin' | 'Please enter a shorter group name' | 'Privacy Policy' + | 'qa-blocked-contacts-settings-item' | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' From 41a4413b58749c652c7620c069301e1802a1d237 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 9 Sep 2025 10:08:16 +1000 Subject: [PATCH 41/88] fix: need to skip self healing for findWithFallback --- run/types/DeviceWrapper.ts | 62 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index ae2bce7f4..66a3be5b4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -390,9 +390,14 @@ export class DeviceWrapper { /** * Finds element with self-healing for id/accessibility id strategies. - * Throws if not found even after healing attempt. + * @param skipHealing - Disable self-healing for this call + * @throws If element not found even after healing attempt. */ - public async findElement(strategy: Strategy, selector: string): Promise { + public async findElement( + strategy: Strategy, + selector: string, + skipHealing = false + ): Promise { try { return await (this.toShared().findElement( strategy, @@ -401,16 +406,16 @@ export class DeviceWrapper { } catch (originalError) { // Only try healing for id/accessibility id selectors // In the future we can think about extracting values from XPATH etc. - if (strategy !== 'accessibility id' && strategy !== 'id') { + if (skipHealing || (strategy !== 'accessibility id' && strategy !== 'id')) { throw originalError; } - const best = await this.findBestMatch(strategy, selector); + const healed = await this.findBestMatch(strategy, selector); - if (best) { + if (healed) { return await (this.toShared().findElement( - best.strategy, - best.selector + healed.strategy, + healed.selector ) as Promise); } @@ -420,11 +425,13 @@ export class DeviceWrapper { /** * Finds elements with self-healing for id/accessibility id strategies. + * @param skipHealing - Disable self-healing for this call * Returns empty array if not found. */ public async findElements( strategy: Strategy, - selector: string + selector: string, + skipHealing = false ): Promise> { const elements = await (this.toShared().findElements(strategy, selector) as Promise< Array @@ -434,7 +441,7 @@ export class DeviceWrapper { } // Only try healing for id/accessibility id selectors // In the future we can think about extracting values from XPATH etc. - if (strategy !== 'accessibility id' && strategy !== 'id') { + if (skipHealing || (strategy !== 'accessibility id' && strategy !== 'id')) { return []; } @@ -477,14 +484,14 @@ export class DeviceWrapper { const fallbackDescription = describeLocator(fallback); try { - return await this.waitForTextElementToBePresent({ ...primary, maxWait }); + return await this.waitForTextElementToBePresent({ ...primary, maxWait, skipHealing: true }); } catch (primaryError) { console.warn( `[findWithFallback] Could not find element with ${primaryDescription}, falling back to ${fallbackDescription}` ); try { - return await this.waitForTextElementToBePresent({ ...fallback, maxWait }); + return await this.waitForTextElementToBePresent({ ...fallback, maxWait, skipHealing: true }); } catch (fallbackError) { throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`); } @@ -1318,7 +1325,7 @@ export class DeviceWrapper { // WAIT FOR FUNCTIONS public async waitForTextElementToBePresent( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) + args: { text?: string; maxWait?: number, skipHealing?: boolean } & (LocatorsInterface | StrategyExtractionObj) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1326,6 +1333,7 @@ export class DeviceWrapper { const text = args.text ?? ('text' in locator ? locator.text : undefined); const { maxWait = 30_000 } = args; + const skipHealing = 'skipHealing' in args ? args.skipHealing ?? false : false; const description = describeLocator({ ...locator, text }); this.log(`Waiting for element with ${description} to be present`); @@ -1336,10 +1344,10 @@ export class DeviceWrapper { let element: AppiumNextElementType | null = null; if (text) { - const els = await this.findElements(locator.strategy, locator.selector); + const els = await this.findElements(locator.strategy, locator.selector, skipHealing); element = await this.findMatchingTextInElementArray(els, text); } else { - element = await this.findElement(locator.strategy, locator.selector); + element = await this.findElement(locator.strategy, locator.selector, skipHealing); } return element @@ -2312,6 +2320,13 @@ export class DeviceWrapper { return; } + /** + * Checks modal heading and description text against expected values. + * Uses fallback locators to support both new (id) and legacy (accessibility id) variants on Android. + * @param expectedHeading - Expected modal heading string + * @param expectedDescription - Expected modal description string + * @throws Error if heading or description doesn't match expected text + */ public async checkModalStrings(expectedHeading: string, expectedDescription: string) { // Sanitize function removeNewLines(input: string): string { @@ -2319,9 +2334,22 @@ export class DeviceWrapper { return input.replace(/\s*\n+/g, ' ').trim(); } - // Locators - const elHeading = await this.waitForTextElementToBePresent(new ModalHeading(this)); - const elDescription = await this.waitForTextElementToBePresent(new ModalDescription(this)); + // Always try new first, fall back to legacy + const newHeading = new ModalHeading(this).build(); + const legacyHeading = { + strategy: 'accessibility id', + selector: 'Modal heading', + } as StrategyExtractionObj; + + const newDescription = new ModalDescription(this).build(); + const legacyDescription = { + strategy: 'accessibility id', + selector: 'Modal description', + } as StrategyExtractionObj; + + // New → legacy fallback + const elHeading = await this.findWithFallback(newHeading, legacyHeading); + const elDescription = await this.findWithFallback(newDescription, legacyDescription); // Modal Heading const actualHeading = removeNewLines(await this.getTextFromElement(elHeading)); From 906990fec2a79351da28fb4b46edf637537f606b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 9 Sep 2025 10:24:00 +1000 Subject: [PATCH 42/88] fix: dedup and sort healing annotations --- run/types/DeviceWrapper.ts | 35 ++++++++++++++++++++++++++--------- run/types/sessionIt.ts | 7 ++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 66a3be5b4..1e3f16ab0 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -372,13 +372,23 @@ export class DeviceWrapper { if (match.strategy === strategy && match.originalSelector === selector) { return null; } - this.log( - `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.selector}" (${confidence}% match)` + + // Check if we've already logged this exact healing + // Only log new healing signatures + const healingSignature = `${strategy} "${selector}" ➡ ${match.strategy} "${match.originalSelector}"`; + const alreadyLogged = this.testInfo.annotations.some( + a => a.type === 'healed' && a.description?.includes(healingSignature) ); - this.testInfo.annotations.push({ - type: 'healed', - description: ` ${strategy} "${selector}" ➡ ${match.strategy} "${match.selector}" (${confidence}% match)`, - }); + + if (!alreadyLogged) { + this.log( + `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.originalSelector}" (${confidence}% match)` + ); + this.testInfo.annotations.push({ + type: 'healed', + description: ` ${healingSignature} (${confidence}% match)`, + }); + } return { strategy: match.strategy, selector: match.originalSelector, @@ -491,7 +501,11 @@ export class DeviceWrapper { ); try { - return await this.waitForTextElementToBePresent({ ...fallback, maxWait, skipHealing: true }); + return await this.waitForTextElementToBePresent({ + ...fallback, + maxWait, + skipHealing: true, + }); } catch (fallbackError) { throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`); } @@ -1325,7 +1339,10 @@ export class DeviceWrapper { // WAIT FOR FUNCTIONS public async waitForTextElementToBePresent( - args: { text?: string; maxWait?: number, skipHealing?: boolean } & (LocatorsInterface | StrategyExtractionObj) + args: { text?: string; maxWait?: number; skipHealing?: boolean } & ( + | LocatorsInterface + | StrategyExtractionObj + ) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1333,7 +1350,7 @@ export class DeviceWrapper { const text = args.text ?? ('text' in locator ? locator.text : undefined); const { maxWait = 30_000 } = args; - const skipHealing = 'skipHealing' in args ? args.skipHealing ?? false : false; + const skipHealing = 'skipHealing' in args ? (args.skipHealing ?? false) : false; const description = describeLocator({ ...locator, text }); this.log(`Waiting for element with ${description} to be present`); diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 1d84cbcbf..d636a6660 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -77,9 +77,14 @@ function mobileIt({ try { await testCb(platform, testInfo); + // If the test passed but used healing, fail loudly to be identified in the allure report const healedAnnotations = testInfo.annotations.filter(a => a.type === 'healed'); if (healedAnnotations.length > 0) { - const details = healedAnnotations.map(a => ` ${a.description}`).join('\n'); + // Deduplicate and sort for consistent error messages + const uniqueHealings = [...new Set(healedAnnotations.map(a => a.description))]; + uniqueHealings.sort(); + + const details = uniqueHealings.join('\n'); throw new Error(`Test passed but used healed locators:\n${details}`); } } catch (error) { From 58cea99eaff2817ff68dc575d326d7b1cf95c6ce Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 9 Sep 2025 10:24:24 +1000 Subject: [PATCH 43/88] chore: remove links, fix tags --- run/test/specs/group_message_long_text.spec.ts | 3 --- run/test/specs/locators/index.ts | 4 ++-- run/test/specs/message_long_text.spec.ts | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index a18943372..70da9c211 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -12,9 +12,6 @@ bothPlatformsIt({ countOfDevicesNeeded: 3, testCb: sendLongMessageGroup, allureDescription: 'Verifies that a long message can be sent to a group', - allureLinks: { - android: 'SES-4337', - }, }); async function sendLongMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index c8b8767db..99734b7b1 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -398,8 +398,8 @@ export class BlockedContactsSettings extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'qa-blocked-contacts-settings-item' + strategy: 'accessibility id', + selector: 'qa-blocked-contacts-settings-item', }; case 'ios': return { diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index 1bde31726..ad4312aff 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -18,9 +18,6 @@ bothPlatformsItSeparate({ android: { testCb: sendLongMessageAndroid, }, - allureLinks: { - android: 'SES-4337', - }, }); async function sendLongMessageIos(platform: SupportedPlatformsType, testInfo: TestInfo) { From 27a1491cce6565665a178892ddcd8c47ad2d5211 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 9 Sep 2025 15:00:50 +1000 Subject: [PATCH 44/88] refactor: longPress takes locator object instead of ax id --- run/test/specs/disappearing_gif.spec.ts | 4 +- run/test/specs/disappearing_video.spec.ts | 5 +- run/test/specs/disappearing_voice.spec.ts | 10 ++-- .../group_disappearing_messages_gif.spec.ts | 5 +- .../group_disappearing_messages_image.spec.ts | 6 +-- .../group_disappearing_messages_video.spec.ts | 6 +-- run/test/specs/group_message_document.spec.ts | 23 ++++---- run/test/specs/group_message_gif.spec.ts | 36 +++++-------- run/test/specs/group_message_image.spec.ts | 17 ++---- run/test/specs/group_message_video.spec.ts | 4 +- run/test/specs/group_message_voice.spec.ts | 9 ++-- run/test/specs/locators/conversation.ts | 52 +++++++++++++++++++ run/test/specs/locators/home.ts | 19 +++++++ run/test/specs/locators/index.ts | 16 ------ run/test/specs/message_document.spec.ts | 10 +--- run/test/specs/message_gif.spec.ts | 8 +-- .../specs/message_requests_decline.spec.ts | 7 +-- .../specs/message_requests_delete.spec.ts | 4 +- run/test/specs/message_video.spec.ts | 4 +- run/test/specs/message_voice.spec.ts | 10 ++-- run/types/DeviceWrapper.ts | 25 +++++---- 21 files changed, 149 insertions(+), 131 deletions(-) diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 19baefe12..8d40e74c1 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { MediaMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -38,8 +39,7 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI await Promise.all( [alice1, bob1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + ...new MediaMessage(device).build(), initialMaxWait, maxWait, preventEarlyDeletion: true, diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9f939cd25..9d4e4f62a 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -53,8 +53,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes await Promise.all( [alice1, bob1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + ...new MediaMessage(device).build(), initialMaxWait, maxWait, preventEarlyDeletion: true, diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index f0e39163a..832cc0815 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -32,14 +33,9 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); await alice1.sendVoiceMessage(); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }); + await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await Promise.all([ - alice1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Voice message', + alice1.hasElementBeenDeleted({...new VoiceMessage(alice1).build(), maxWait, preventEarlyDeletion: true, }), diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index e2c25ad9d..d97f91a5b 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -2,6 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MediaMessage } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -43,8 +44,8 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + ...new MediaMessage(device).build(), + initialMaxWait, maxWait, preventEarlyDeletion: true, diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index a388a8dd6..631aa7628 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -56,8 +56,8 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + ...new MediaMessage(device).build(), + maxWait, preventEarlyDeletion: true, }) diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 8a7b698f7..54b67b64b 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -56,8 +56,8 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Media message', + ...new MediaMessage(device).build(), + initialMaxWait, maxWait, preventEarlyDeletion: true, diff --git a/run/test/specs/group_message_document.spec.ts b/run/test/specs/group_message_document.spec.ts index fbec7d80b..edd6378b2 100644 --- a/run/test/specs/group_message_document.spec.ts +++ b/run/test/specs/group_message_document.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { DocumentMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -66,18 +66,15 @@ async function sendDocumentGroupAndroid(platform: SupportedPlatformsType, testIn charlie1.trustAttachments(testGroupName), ]); // Check document appears in both device 2 and 3's screen - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }), - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }), - ]); - // Reply to document from user B - await bob1.longPress('Document'); + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new DocumentMessage(device)) + ) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await sleepFor(1000); + await bob1.longPress(new DocumentMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply from device 2 came through on alice1 and charlie1 diff --git a/run/test/specs/group_message_gif.spec.ts b/run/test/specs/group_message_gif.spec.ts index fae04e543..ae3a5f5b6 100644 --- a/run/test/specs/group_message_gif.spec.ts +++ b/run/test/specs/group_message_gif.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -35,15 +35,13 @@ async function sendGifGroupiOS(platform: SupportedPlatformsType, testInfo: TestI await alice1.sendGIF(); await sleepFor(500); - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await bob1.longPress('Media message'); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await sleepFor(1000); + await bob1.longPress(new MediaMessage(bob1)); // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); @@ -74,18 +72,12 @@ async function sendGifGroupAndroid(platform: SupportedPlatformsType, testInfo: T bob1.trustAttachments(testGroupName), charlie1.trustAttachments(testGroupName), ]); - // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - maxWait: 10000, - }); - await charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - maxWait: 10000, - }); - await bob1.longPress('Media message'); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); + // Reply to image - user B + // Sleep for is waiting for image to load + await bob1.longPress(new MediaMessage(bob1)); // Check reply came through on alice1 await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); diff --git a/run/test/specs/group_message_image.spec.ts b/run/test/specs/group_message_image.spec.ts index 92be52112..61fa50522 100644 --- a/run/test/specs/group_message_image.spec.ts +++ b/run/test/specs/group_message_image.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody, OutgoingMessageStatusSent } from './locators/conversation'; +import { MediaMessage, MessageBody, OutgoingMessageStatusSent } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -78,20 +78,13 @@ async function sendImageGroupAndroid(platform: SupportedPlatformsType, testInfo: bob1.trustAttachments(testGroupName), charlie1.trustAttachments(testGroupName), ]); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }), - charlie1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }), - ]); + await Promise.all( + [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MediaMessage(device))) + ); // Reply to image - user B // Sleep for is waiting for image to load await sleepFor(1000); - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await Promise.all( diff --git a/run/test/specs/group_message_video.spec.ts b/run/test/specs/group_message_video.spec.ts index 8cc670414..c3d7af6e8 100644 --- a/run/test/specs/group_message_video.spec.ts +++ b/run/test/specs/group_message_video.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -91,7 +91,7 @@ async function sendVideoGroupAndroid(platform: SupportedPlatformsType, testInfo: }), ]); // Reply to message on device 2 - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); // Check reply appears in device 1 and device 3 diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index f3ce8bcec..2efd9bf0a 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MessageBody, VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -30,13 +30,10 @@ async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: ); await Promise.all( [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }) + device.waitForTextElementToBePresent(new VoiceMessage(device)) ) ); - await bob1.longPress('Voice message'); + await bob1.longPress(new VoiceMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await Promise.all( diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 763b9a6e4..5f09063ce 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -23,6 +23,19 @@ export class SendButton extends LocatorsInterface { } } +export class NewVoiceMessageButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'New voice message', + } as const; + } + } +} + export class MessageBody extends LocatorsInterface { public text: string | undefined; constructor(device: DeviceWrapper, text?: string) { @@ -42,6 +55,45 @@ export class MessageBody extends LocatorsInterface { } } +export class VoiceMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Voice message', + } as const; + } + } +} + +export class MediaMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Media message', + } as const; + } + } +} + +export class DocumentMessage extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Document', + } as const; + } + } +} + export class ScrollToBottomButton extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index bcbe980e4..df2c090c3 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -52,6 +52,25 @@ export class ConversationItem extends LocatorsInterface { } } +export class MessageRequestItem extends LocatorsInterface { + public text?: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Message request', + text: this.text, + } as const; + } + } +} + // For identifying a conversation with a specific last message in it export class MessageSnippet extends LocatorsInterface { public conversationName: string; diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 99734b7b1..774d20e16 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -193,22 +193,6 @@ export class FirstGif extends LocatorsInterface { } } -export class MediaMessage extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'accessibility id', - selector: 'Media message', - }; - case 'ios': - return { - strategy: 'class name', - selector: 'XCUIElementTypeImage', - }; - } - } -} export class BlockUser extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/specs/message_document.spec.ts b/run/test/specs/message_document.spec.ts index fc7422e7c..5c7b67a7e 100644 --- a/run/test/specs/message_document.spec.ts +++ b/run/test/specs/message_document.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { DocumentMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,14 +25,8 @@ async function sendDocument(platform: SupportedPlatformsType, testInfo: TestInfo await alice1.sendDocument(); await bob1.trustAttachments(alice.userName); - // Reply to message - await bob1.onIOS().waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); - await bob1.onAndroid().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Document', - }); await bob1.onIOS().longPressMessage(testMessage); - await bob1.onAndroid().longPress('Document'); + await bob1.onAndroid().longPress(new DocumentMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); diff --git a/run/test/specs/message_gif.spec.ts b/run/test/specs/message_gif.spec.ts index 1c8c8e887..16a24ecca 100644 --- a/run/test/specs/message_gif.spec.ts +++ b/run/test/specs/message_gif.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -25,11 +25,7 @@ async function sendGif(platform: SupportedPlatformsType, testInfo: TestInfo) { await alice1.sendGIF(); await bob1.trustAttachments(alice.userName); // Reply to message - await bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Media message', - }); - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index 7e7c87235..9588cfa82 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { DeclineMessageRequestButton, DeleteMesssageRequestConfirmation } from './locators'; -import { MessageRequestsBanner, PlusButton } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner, PlusButton } from './locators/home'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; @@ -32,10 +32,7 @@ async function declineRequest(platform: SupportedPlatformsType, testInfo: TestIn await device2.clickOnByAccessibilityID('Message request'); // Check message request appears on linked device (device 3) await device3.clickOnElementAll(new MessageRequestsBanner(device3)); - await device3.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message request', - }); + await device3.waitForTextElementToBePresent(new MessageRequestItem(device3)); // Click on decline button await device2.clickOnElementAll(new DeclineMessageRequestButton(device2)); // Are you sure you want to delete message request only for ios diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index d8233664e..d6dd6b901 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { DeleteMessageRequestButton, DeleteMesssageRequestConfirmation } from './locators'; -import { MessageRequestsBanner } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; @@ -28,7 +28,7 @@ async function deleteRequest(platform: SupportedPlatformsType, testInfo: TestInf await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Swipe left on ios await device2.onIOS().swipeLeftAny('Message request'); - await device2.onAndroid().longPress('Message request'); + await device2.onAndroid().longPress(new MessageRequestItem(device2)); await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); // TODO remove onIOS/onAndroid once SES-3846 has been completed await device2 diff --git a/run/test/specs/message_video.spec.ts b/run/test/specs/message_video.spec.ts index f2357122f..42b2a0bfb 100644 --- a/run/test/specs/message_video.spec.ts +++ b/run/test/specs/message_video.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -65,7 +65,7 @@ async function sendVideoAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'id', selector: 'network.loki.messenger.qa:id/play_overlay', }); - await bob1.longPress('Media message'); + await bob1.longPress(new MediaMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await sleepFor(2000); diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index b786bc127..4442bdc03 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { MessageBody, VoiceMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -26,14 +26,10 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test // Select voice message button to activate recording state await alice1.sendVoiceMessage(); await sleepFor(500); - await alice1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Voice message', - }); - + await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await bob1.trustAttachments(alice.userName); await sleepFor(500); - await bob1.longPress('Voice message'); + await bob1.longPress(new VoiceMessage(bob1)); await bob1.clickOnByAccessibilityID('Reply to message'); await bob1.sendMessage(replyMessage); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 1e3f16ab0..95ad11de2 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -33,6 +33,7 @@ import { AttachmentsButton, MessageBody, MessageInput, + NewVoiceMessageButton, OutgoingMessageStatusSent, ScrollToBottomButton, SendButton, @@ -617,17 +618,21 @@ export class DeviceWrapper { } await this.click(el.ELEMENT); } - // TODO update this function to handle new locator logic - public async longPress(accessibilityId: AccessibilityId, text?: string) { - const el = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: accessibilityId, - text, - }); + public async longPress( + args: { text?: string; duration?: number } & (LocatorsInterface | StrategyExtractionObj) + ): Promise { + const { text, duration = 2000 } = args; + const locator = args instanceof LocatorsInterface ? args.build() : args; + // Merge text if provided + const finalLocator = text ? { ...locator, text } : locator; + + const el = await this.waitForTextElementToBePresent({ ...finalLocator }); if (!el) { - throw new Error(`longPress: Could not find accessibilityId: ${accessibilityId}`); + const description = describeLocator(finalLocator); + throw new Error(`longPress: Could not find element: ${description}`); } - await this.longClick(el, 2000); + + await this.longClick(el, duration); } public async longPressMessage(textToLookFor: string) { @@ -1985,7 +1990,7 @@ export class DeviceWrapper { } public async sendVoiceMessage() { - await this.longPress('New voice message'); + await this.longPress(new NewVoiceMessageButton(this)); if (this.isAndroid()) { await this.clickOnElementAll({ From 32271b842f39002c3b209a733b88ba7cf9210910 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 9 Sep 2025 16:59:16 +1000 Subject: [PATCH 45/88] wip: healing should consider text --- run/types/DeviceWrapper.ts | 146 ++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 43 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 95ad11de2..edb539f35 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -313,7 +313,8 @@ export class DeviceWrapper { // Heal a broken locator by finding potential fuzzy matches in the page source and log it for a permanent fix. private async findBestMatch( strategy: Strategy, - selector: string + selector: string, + text?: string ): Promise<{ strategy: Strategy; selector: string } | null> { const pageSource = await this.getPageSource(); const threshold = 0.35; // 0.0 = exact, 1.0 = match anything @@ -373,7 +374,49 @@ export class DeviceWrapper { if (match.strategy === strategy && match.originalSelector === selector) { return null; } + // If expectedText is provided, validate that the healed element(s) contain it + if (text) { + try { + // Call raw Appium method directly to avoid recursion + const healedElements = await (this.toShared().findElements( + match.strategy, + match.originalSelector + ) as Promise>); + + if (healedElements && healedElements.length > 0) { + let hasExpectedText = false; + for (const element of healedElements) { + try { + const elementText = await this.getTextFromElement(element); + if (elementText.includes(text)) { + hasExpectedText = true; + break; + } + } catch (e) { + // Skip elements that can't provide text + continue; + } + } + if (!hasExpectedText) { + this.log( + `Potential heal found (${match.strategy} "${match.originalSelector}") but none contain expected text "${text}". Skipping this match.` + ); + return null; // This heal is invalid + } + } else { + this.log( + `Potential heal found (${match.strategy} "${match.originalSelector}") but no elements found. Skipping this match.` + ); + return null; + } + } catch (e) { + this.log( + `Error validating potential heal (${match.strategy} "${match.originalSelector}"): ${String(e)}. Skipping this match.` + ); + return null; + } + } // Check if we've already logged this exact healing // Only log new healing signatures const healingSignature = `${strategy} "${selector}" ➡ ${match.strategy} "${match.originalSelector}"`; @@ -437,12 +480,14 @@ export class DeviceWrapper { /** * Finds elements with self-healing for id/accessibility id strategies. * @param skipHealing - Disable self-healing for this call + * @param expectedText - If provided, validates that at least one healed element contains this text * Returns empty array if not found. */ public async findElements( strategy: Strategy, selector: string, - skipHealing = false + skipHealing = false, + expectedText?: string ): Promise> { const elements = await (this.toShared().findElements(strategy, selector) as Promise< Array @@ -450,13 +495,13 @@ export class DeviceWrapper { if (elements && elements.length > 0) { return elements; } + // Only try healing for id/accessibility id selectors - // In the future we can think about extracting values from XPATH etc. if (skipHealing || (strategy !== 'accessibility id' && strategy !== 'id')) { return []; } - const healed = await this.findBestMatch(strategy, selector); + const healed = await this.findBestMatch(strategy, selector, expectedText); if (healed) { return ( @@ -627,10 +672,6 @@ export class DeviceWrapper { const finalLocator = text ? { ...locator, text } : locator; const el = await this.waitForTextElementToBePresent({ ...finalLocator }); - if (!el) { - const description = describeLocator(finalLocator); - throw new Error(`longPress: Could not find element: ${description}`); - } await this.longClick(el, duration); } @@ -1083,7 +1124,11 @@ export class DeviceWrapper { args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) ): Promise { try { - return await this.waitForTextElementToBePresent(args); + const locatorArgs = + args instanceof LocatorsInterface + ? { ...args.build(), text: args.text, maxWait: args.maxWait, skipHealing: true } + : { ...args, skipHealing: true }; + return await this.waitForTextElementToBePresent(locatorArgs); } catch { return null; } @@ -1360,37 +1405,55 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text }); this.log(`Waiting for element with ${description} to be present`); - const result = await this.pollUntil( - async () => { - try { - let element: AppiumNextElementType | null = null; - - if (text) { - const els = await this.findElements(locator.strategy, locator.selector, skipHealing); - element = await this.findMatchingTextInElementArray(els, text); - } else { - element = await this.findElement(locator.strategy, locator.selector, skipHealing); - } - + // Helper function to find element with or without healing + const tryFindElement = async (allowHealing: boolean): Promise => { + try { + if (text) { + const els = await this.findElements( + locator.strategy, + locator.selector, + !allowHealing, + text + ); + return await this.findMatchingTextInElementArray(els, text); + } + return await this.findElement(locator.strategy, locator.selector, !allowHealing); + } catch (err) { + return null; + } + }; + try { + // Clean polling without healing - let pollUntil handle error reporting + const result = await this.pollUntil( + async () => { + const element = await tryFindElement(false); // No healing during polling return element ? { success: true, data: element } : { success: false, error: `Element with ${description} not found` }; - } catch (err) { - return { - success: false, - error: `Element with ${description} not found`, - }; - } - }, - { maxWait } - ); + }, + { maxWait } + ); - if (!result) { - throw new Error(`Waited too long for element with ${description}`); - } + this.log(`Element with ${description} has been found`); + return result!; // result exists because pollUntil succeeded + } catch (originalError) { + // If healing is disabled, just throw the original error + if (skipHealing) { + throw originalError; + } - this.log(`Element with ${description} has been found`); - return result; + // Try healing as last resort + this.log(`Polling failed. Attempting self-healing as last resort...`); + + const element = await tryFindElement(true); // Allow healing + if (element) { + this.log(`Self-healing successful! Element with ${description} found via healing`); + return element; + } + + // Re-throw original error with all the pollUntil details intact + throw originalError; + } } public async waitForControlMessageToBePresent( @@ -2176,17 +2239,14 @@ export class DeviceWrapper { } public async scrollToBottom() { - try { - const scrollButton = await this.waitForTextElementToBePresent({ - ...new ScrollToBottomButton(this).build(), - maxWait: 3_000, - }); - await this.click(scrollButton.ELEMENT); - } catch { + if ( + await this.doesElementExist({ ...new ScrollToBottomButton(this).build(), maxWait: 3_000 }) + ) { + await this.clickOnElementAll(new ScrollToBottomButton(this)); + } else { this.info('Scroll button not found, continuing'); } } - public async pullToRefresh() { if (this.isAndroid()) { await this.pressCoordinates( From 4af82bc6b3101a25b88e57038f28c93a8205f7c1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 10 Sep 2025 09:58:06 +1000 Subject: [PATCH 46/88] fix: text must match for healing to kick in --- run/types/DeviceWrapper.ts | 138 ++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index edb539f35..fc3a1a52b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -309,8 +309,7 @@ export class DeviceWrapper { /* === all the device-specific function === */ // ELEMENT INTERACTION - - // Heal a broken locator by finding potential fuzzy matches in the page source and log it for a permanent fix. + // Heal a broken locator by finding potential fuzzy matches with text as first-class criteria private async findBestMatch( strategy: Strategy, selector: string, @@ -366,77 +365,77 @@ export class DeviceWrapper { const results = fuse.search(stripPrefix(selector)); - if (results.length > 0 && results[0].score !== undefined && results[0].score < threshold) { - const match = results[0].item; - const confidence = ((1 - results[0].score) * 100).toFixed(2); + // Evaluate each candidate with BOTH selector similarity AND text content + for (const result of results) { + if (result.score === undefined || result.score >= threshold) continue; + + const match = result.item; + const selectorConfidence = ((1 - result.score) * 100).toFixed(2); - // Sometimes the element is just not on screen yet - proceed. + // Sometimes the element is just not on screen yet - skip if (match.strategy === strategy && match.originalSelector === selector) { - return null; + continue; } - // If expectedText is provided, validate that the healed element(s) contain it + + // If we need text validation, check it as part of matching criteria + let textMatches = true; if (text) { try { - // Call raw Appium method directly to avoid recursion const healedElements = await (this.toShared().findElements( match.strategy, match.originalSelector ) as Promise>); if (healedElements && healedElements.length > 0) { - let hasExpectedText = false; + textMatches = false; // Assume no match until proven otherwise for (const element of healedElements) { try { const elementText = await this.getTextFromElement(element); if (elementText.includes(text)) { - hasExpectedText = true; + textMatches = true; break; } } catch (e) { - // Skip elements that can't provide text - continue; + continue; // Skip elements that can't provide text } } - - if (!hasExpectedText) { - this.log( - `Potential heal found (${match.strategy} "${match.originalSelector}") but none contain expected text "${text}". Skipping this match.` - ); - return null; // This heal is invalid - } } else { - this.log( - `Potential heal found (${match.strategy} "${match.originalSelector}") but no elements found. Skipping this match.` - ); - return null; + textMatches = false; // No elements found } } catch (e) { + textMatches = false; // Error getting elements + } + } + + // Only accept candidates that pass BOTH selector similarity AND text content + if (textMatches) { + // Check if we've already logged this exact healing + // Only log new healing signatures + const healingSignature = `${strategy} "${selector}" ➡ ${match.strategy} "${match.originalSelector}"`; + const alreadyLogged = this.testInfo.annotations.some( + a => a.type === 'healed' && a.description?.includes(healingSignature) + ); + + if (!alreadyLogged) { this.log( - `Error validating potential heal (${match.strategy} "${match.originalSelector}"): ${String(e)}. Skipping this match.` + `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.originalSelector}" (${selectorConfidence}% match)` ); - return null; + this.testInfo.annotations.push({ + type: 'healed', + description: ` ${healingSignature} (${selectorConfidence}% match)`, + }); } - } - // Check if we've already logged this exact healing - // Only log new healing signatures - const healingSignature = `${strategy} "${selector}" ➡ ${match.strategy} "${match.originalSelector}"`; - const alreadyLogged = this.testInfo.annotations.some( - a => a.type === 'healed' && a.description?.includes(healingSignature) - ); - if (!alreadyLogged) { + return { + strategy: match.strategy, + selector: match.originalSelector, + }; + } else if (text) { + // Log why this candidate was rejected this.log( - `Original locator ${strategy} "${selector}" not found. Test healed with ${match.strategy} "${match.originalSelector}" (${confidence}% match)` + `Candidate ${match.strategy} "${match.originalSelector}" (${selectorConfidence}% match) rejected: missing text "${text}"` ); - this.testInfo.annotations.push({ - type: 'healed', - description: ` ${healingSignature} (${confidence}% match)`, - }); } - return { - strategy: match.strategy, - selector: match.originalSelector, - }; } return null; @@ -1388,6 +1387,14 @@ export class DeviceWrapper { } // WAIT FOR FUNCTIONS +/** + * Waits for an element to be present with optional text matching and self-healing. + * Continuously polls for maxWait seconds, then attempts healing as last resort if not found. + * + * @param args - Locator and options (text, maxWait, skipHealing) + * @returns Promise resolving to the found element + * @throws If element not found + */ public async waitForTextElementToBePresent( args: { text?: string; maxWait?: number; skipHealing?: boolean } & ( | LocatorsInterface @@ -1422,38 +1429,31 @@ export class DeviceWrapper { return null; } }; - try { - // Clean polling without healing - let pollUntil handle error reporting - const result = await this.pollUntil( - async () => { - const element = await tryFindElement(false); // No healing during polling - return element - ? { success: true, data: element } - : { success: false, error: `Element with ${description} not found` }; - }, - { maxWait } - ); - - this.log(`Element with ${description} has been found`); - return result!; // result exists because pollUntil succeeded - } catch (originalError) { - // If healing is disabled, just throw the original error - if (skipHealing) { - throw originalError; - } - // Try healing as last resort - this.log(`Polling failed. Attempting self-healing as last resort...`); + const result = await this.pollUntil( + async () => { + const element = await tryFindElement(false); // No healing during polling + return element + ? { success: true, data: element } + : { success: false, error: `Element with ${description} not found` }; + }, + { maxWait } + ).catch(async originalError => { + // If healing is disabled, re-throw original error + if (skipHealing) throw originalError; - const element = await tryFindElement(true); // Allow healing + // One attempt at healing after polling fails + const element = await tryFindElement(true); if (element) { - this.log(`Self-healing successful! Element with ${description} found via healing`); + // Healing succeeded return element; } - - // Re-throw original error with all the pollUntil details intact + // Healing failed, re-throw original error throw originalError; - } + }); + // Element was found as-is + this.log(`Element with ${description} has been found`); + return result!; // Result must exist if we reached this point } public async waitForControlMessageToBePresent( From 7840ffad22785c1f4f07b5ae50d1d6495e2efc4e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 10 Sep 2025 14:20:52 +1000 Subject: [PATCH 47/88] fix: don't show blocking ANRs --- run/test/specs/utils/open_app.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index 291c8dd33..ac427e820 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -268,6 +268,10 @@ const openAndroidApp = async ( `); await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put global animator_duration_scale 0 `); + await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put global show_first_crash_dialog 0 + `); + await runScriptAndLog(`${getAdbFullPath()} -s ${targetName} shell settings put secure anr_show_background 0 +`); await wrappedDevice.createSession(capabilities); From b9f01457c9d64fe3150db117b486ecff81cf5eb4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 10 Sep 2025 14:21:14 +1000 Subject: [PATCH 48/88] chore: linting --- run/test/specs/disappearing_voice.spec.ts | 3 ++- run/test/specs/locators/index.ts | 2 +- run/types/DeviceWrapper.ts | 16 ++++++++-------- run/types/testing.ts | 4 ++++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index 832cc0815..3285cac5c 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -35,7 +35,8 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes await alice1.sendVoiceMessage(); await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await Promise.all([ - alice1.hasElementBeenDeleted({...new VoiceMessage(alice1).build(), + alice1.hasElementBeenDeleted({ + ...new VoiceMessage(alice1).build(), maxWait, preventEarlyDeletion: true, }), diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 774d20e16..d2dc4ce1c 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -388,7 +388,7 @@ export class BlockedContactsSettings extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Blocked Contacts', + selector: 'View and manage blocked contacts.', // temporary fix }; } } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index fc3a1a52b..064e05760 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1387,14 +1387,14 @@ export class DeviceWrapper { } // WAIT FOR FUNCTIONS -/** - * Waits for an element to be present with optional text matching and self-healing. - * Continuously polls for maxWait seconds, then attempts healing as last resort if not found. - * - * @param args - Locator and options (text, maxWait, skipHealing) - * @returns Promise resolving to the found element - * @throws If element not found - */ + /** + * Waits for an element to be present with optional text matching and self-healing. + * Continuously polls for maxWait seconds, then attempts healing as last resort if not found. + * + * @param args - Locator and options (text, maxWait, skipHealing) + * @returns Promise resolving to the found element + * @throws If element not found + */ public async waitForTextElementToBePresent( args: { text?: string; maxWait?: number; skipHealing?: boolean } & ( | LocatorsInterface diff --git a/run/types/testing.ts b/run/types/testing.ts index a1f73650f..392251ff9 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -402,6 +402,7 @@ export type AccessibilityId = | 'User settings' | 'Version warning banner' | 'Videos' + | 'View and manage blocked contacts.' | 'Voice and Video Calls - Switch' | 'Voice message' | 'X' @@ -415,6 +416,9 @@ export type AccessibilityId = export type Id = | DISAPPEARING_TIMES | 'Account ID' + | 'android:id/aerr_close' + | 'android:id/aerr_wait' + | 'android:id/alertTitle' | 'android:id/content_preview_text' | 'android:id/summary' | 'android:id/text1' From dd0f3401fbe181c2ea93bf95cab6b78ef3f8b3e5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 10 Sep 2025 14:58:29 +1000 Subject: [PATCH 49/88] fix: findElementQuietly shouldn't heal --- run/types/DeviceWrapper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 064e05760..42b07a3ea 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1325,7 +1325,7 @@ export class DeviceWrapper { } /** - * Find an element without throwing errors or logging. + * Find an element without throwing errors, logging or healing. */ private async findElementQuietly( locator: StrategyExtractionObj, @@ -1333,7 +1333,7 @@ export class DeviceWrapper { ): Promise { try { if (text) { - const elements = await this.findElements(locator.strategy, locator.selector); + const elements = await this.findElements(locator.strategy, locator.selector, true); for (const element of elements) { const elementText = await this.getText(element.ELEMENT); if (elementText && elementText.toLowerCase() === text.toLowerCase()) { @@ -1342,7 +1342,7 @@ export class DeviceWrapper { } return null; } - return await this.findElement(locator.strategy, locator.selector); + return await this.findElement(locator.strategy, locator.selector, true); } catch { return null; } From f9a32a612cfc982168af2b72c0c3fcd12067599f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 10 Sep 2025 16:35:47 +1000 Subject: [PATCH 50/88] feat: add blacklist to healing --- run/types/DeviceWrapper.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 42b07a3ea..c6663f5a5 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -309,6 +309,7 @@ export class DeviceWrapper { /* === all the device-specific function === */ // ELEMENT INTERACTION + // Heal a broken locator by finding potential fuzzy matches with text as first-class criteria private async findBestMatch( strategy: Strategy, @@ -328,6 +329,10 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; + const blacklist = [ + { from: 'Voice message', to: 'New voice message'}, + ] + // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores // Strip any known prefix patterns first const stripPrefix = (selector: string) => { @@ -372,6 +377,17 @@ export class DeviceWrapper { const match = result.item; const selectorConfidence = ((1 - result.score) * 100).toFixed(2); + const isBlacklisted = blacklist.some( + pair => + (selector.includes(pair.from) && match.originalSelector.includes(pair.to) || + selector.includes(pair.to) && match.originalSelector.includes(pair.from)) + ); + + // Don't heal blacklisted pairs + if (isBlacklisted) { + continue; + } + // Sometimes the element is just not on screen yet - skip if (match.strategy === strategy && match.originalSelector === selector) { continue; From 19f67f66c39f22c7a0d776bff9d9b02eb0293df0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 11 Sep 2025 13:36:55 +1000 Subject: [PATCH 51/88] fix: correct accessibility id on ios --- run/test/specs/locators/index.ts | 2 +- run/test/specs/locators/settings.ts | 18 ------------------ run/types/testing.ts | 4 +--- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index d2dc4ce1c..2939f355a 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -388,7 +388,7 @@ export class BlockedContactsSettings extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'View and manage blocked contacts.', // temporary fix + selector: 'Block contacts - Navigation' }; } } diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 319232323..ec4c556e8 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -145,24 +145,6 @@ export class SaveNameChangeButton extends LocatorsInterface { } } -export class BlockedContacts extends LocatorsInterface { - public build(text?: string) { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Contact', - text, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Blocked contacts', - text, - } as const; - } - } -} export class PrivacyMenuItem extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/types/testing.ts b/run/types/testing.ts index 392251ff9..5b3b5dc3f 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -191,10 +191,9 @@ export type AccessibilityId = | 'back' | 'Back' | 'Block' + | 'Block contacts - Navigation' | 'blocked-banner' | 'Blocked banner' - | 'Blocked contacts' - | 'Blocked Contacts' | 'Block message request' | 'Browse' | 'Call' @@ -402,7 +401,6 @@ export type AccessibilityId = | 'User settings' | 'Version warning banner' | 'Videos' - | 'View and manage blocked contacts.' | 'Voice and Video Calls - Switch' | 'Voice message' | 'X' From 05500d9ab3648b90f2621c55f55b5f740dc3f690 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 11 Sep 2025 13:37:09 +1000 Subject: [PATCH 52/88] fix: disappearing image doesn't wait for trustAttachments --- .../group_disappearing_messages_image.spec.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 631aa7628..d0ec55817 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -35,12 +35,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - // await device1.navigateBack(); await alice1.sendImage(testMessage); - await Promise.all([ - bob1.onAndroid().trustAttachments(testGroupName), - charlie1.onAndroid().trustAttachments(testGroupName), - ]); if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => @@ -52,17 +47,23 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t ) ); } - if (platform === 'android') { - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - ...new MediaMessage(device).build(), - - maxWait, - preventEarlyDeletion: true, - }) - ) - ); - } +if (platform === 'android') { + await Promise.all([ + alice1.hasElementBeenDeleted({ + ...new MediaMessage(alice1).build(), + maxWait, + preventEarlyDeletion: true, + }), + // Bob and Charlie haven't trusted the message + ...[bob1, charlie1].map(device => + device.hasElementBeenDeleted({ + strategy: 'accessibility id', + selector: 'Untrusted attachment message', + maxWait, + preventEarlyDeletion: true, + }) + ) + ]); +} await closeApp(alice1, bob1, charlie1); } From 5fd81bec11ab37793e7a891e869db13cf7f7ee6b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 18 Sep 2025 16:35:22 +1000 Subject: [PATCH 53/88] fix: find the emoji reacts belonging to the correct message --- run/test/specs/community_emoji_react.spec.ts | 2 +- run/test/specs/group_reaction.spec.ts | 4 +-- run/test/specs/locators/conversation.ts | 37 ++++++++++++-------- run/test/specs/message_reaction.spec.ts | 2 +- run/types/testing.ts | 6 +++- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index bcc656564..5d3dbca26 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -51,7 +51,7 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { await Promise.all( [alice1, bob1].map(device => - device.waitForTextElementToBePresent(new EmojiReactsPill(device)) + device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)) ) ); }); diff --git a/run/test/specs/group_reaction.spec.ts b/run/test/specs/group_reaction.spec.ts index 496a450f6..269397caa 100644 --- a/run/test/specs/group_reaction.spec.ts +++ b/run/test/specs/group_reaction.spec.ts @@ -53,8 +53,8 @@ async function sendEmojiReactionGroup(platform: SupportedPlatformsType, testInfo // All clients witness emoji and "2" count await Promise.all( [alice1, bob1, charlie1].map(async device => { - await device.waitForTextElementToBePresent(new EmojiReactsPill(device)); - await device.waitForTextElementToBePresent(new EmojiReactsCount(device)); + await device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)); + await device.waitForTextElementToBePresent(new EmojiReactsCount(device, message, '2')); }) ); }); diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 5f09063ce..3bd38bc44 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -526,36 +526,45 @@ export class FirstEmojiReact extends LocatorsInterface { } } +// Find the reactions pill underneath a specific message export class EmojiReactsPill extends LocatorsInterface { - public build() { + constructor(device: DeviceWrapper, private messageText: string) { + super(device); + } + + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'network.loki.messenger.qa:id/layout_emoji_container', - } as const; + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]` + } as const; case 'ios': return { - strategy: 'accessibility id', - selector: '😂', + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]` } as const; + } } -} +}; export class EmojiReactsCount extends LocatorsInterface { - public build() { + constructor(device: DeviceWrapper, private messageText: string, private expectedCount: string = '2') { + super(device); + } + + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'network.loki.messenger.qa:id/reactions_pill_count', - text: '2', + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${this.expectedCount}"]` } as const; case 'ios': return { - strategy: 'accessibility id', - selector: '2', + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]` } as const; } } diff --git a/run/test/specs/message_reaction.spec.ts b/run/test/specs/message_reaction.spec.ts index aa7b4b78a..bf59aa9ea 100644 --- a/run/test/specs/message_reaction.spec.ts +++ b/run/test/specs/message_reaction.spec.ts @@ -46,7 +46,7 @@ async function sendEmojiReaction(platform: SupportedPlatformsType, testInfo: Tes await test.step(TestSteps.VERIFY.EMOJI_REACT, async () => { await Promise.all( [alice1, bob1].map(device => - device.waitForTextElementToBePresent(new EmojiReactsPill(device)) + device.waitForTextElementToBePresent(new EmojiReactsPill(device, message)) ) ); }); diff --git a/run/types/testing.ts b/run/types/testing.ts index 5b3b5dc3f..47f808d1f 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -134,11 +134,15 @@ export type XPath = | `//*[./*[@name='${DISAPPEARING_TIMES}']]/*[2]` | `//*[@resource-id='network.loki.messenger.qa:id/callTitle' and contains(@text, ':')]` | `//*[starts-with(@content-desc, "Photo taken on")]` + | `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]` + | `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${string}"]` | `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${string}"]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/snippetTextView" and @text="${string}"]` | `//android.widget.TextView[@text="${string}"]` | `//XCUIElementTypeAlert//*//XCUIElementTypeButton` | `//XCUIElementTypeButton[@name="Continue"]` | `//XCUIElementTypeButton[@name="Settings"]` + | `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${string}"]]//XCUIElementTypeStaticText[@value="😂"]` + | `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${string}"]]//XCUIElementTypeStaticText[@value="${string}"]` | `//XCUIElementTypeCell[@name="${string}"]` | `//XCUIElementTypeCell[@name="Conversation list item" and @label="${string}"]//XCUIElementTypeStaticText[@name="${string}"]` | `//XCUIElementTypeCell[@name="Session"]` @@ -164,7 +168,7 @@ export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().text("Enter your display name")' | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` - | `new UiSelector().text(${string})`; + | `new UiSelector().text(${string})` export type AccessibilityId = | DISAPPEARING_TIMES From 0d23a0ee8216185a234f7d69e47085f7a9608309 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 18 Sep 2025 16:35:40 +1000 Subject: [PATCH 54/88] fix: disable blind scrollToBottom in sendMessage --- 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 c6663f5a5..2f0e0dbfb 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1638,7 +1638,7 @@ export class DeviceWrapper { throw new Error('Send button not found: Need to restart iOS emulator: Known issue'); } // Might need to scroll down if the message is too long - await this.scrollToBottom(); + // await this.scrollToBottom(); TODO temporarily disabling this to verify // Wait for tick await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), From 6d17376c8ec5f84146a85eb863b1dec2be299ff5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 10:28:39 +1000 Subject: [PATCH 55/88] feat: both platforms use same string now --- run/test/specs/message_requests_delete.spec.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index d6dd6b901..a67a40d35 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -30,19 +30,10 @@ async function deleteRequest(platform: SupportedPlatformsType, testInfo: TestInf await device2.onIOS().swipeLeftAny('Message request'); await device2.onAndroid().longPress(new MessageRequestItem(device2)); await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); - // TODO remove onIOS/onAndroid once SES-3846 has been completed - await device2 - .onIOS() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString() - ); - await device2 - .onAndroid() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString() - ); + await device2.checkModalStrings( + englishStrippedStr('delete').toString(), + englishStrippedStr('messageRequestsContactDelete').toString() + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); From dc2458419768d545748141e8d4dfa91648bc6890 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 10:29:28 +1000 Subject: [PATCH 56/88] chore: linting --- .../group_disappearing_messages_image.spec.ts | 32 +++++++++---------- run/test/specs/locators/conversation.ts | 30 ++++++++++------- run/test/specs/locators/index.ts | 2 +- run/types/DeviceWrapper.ts | 14 ++++---- run/types/testing.ts | 2 +- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index d0ec55817..2081defa8 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -47,23 +47,23 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t ) ); } -if (platform === 'android') { - await Promise.all([ - alice1.hasElementBeenDeleted({ - ...new MediaMessage(alice1).build(), - maxWait, - preventEarlyDeletion: true, - }), - // Bob and Charlie haven't trusted the message - ...[bob1, charlie1].map(device => - device.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', + if (platform === 'android') { + await Promise.all([ + alice1.hasElementBeenDeleted({ + ...new MediaMessage(alice1).build(), maxWait, preventEarlyDeletion: true, - }) - ) - ]); -} + }), + // Bob and Charlie haven't trusted the message + ...[bob1, charlie1].map(device => + device.hasElementBeenDeleted({ + strategy: 'accessibility id', + selector: 'Untrusted attachment message', + maxWait, + preventEarlyDeletion: true, + }) + ), + ]); + } await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 3bd38bc44..2d81e2423 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -526,31 +526,37 @@ export class FirstEmojiReact extends LocatorsInterface { } } -// Find the reactions pill underneath a specific message +// Find the reactions pill underneath a specific message export class EmojiReactsPill extends LocatorsInterface { - constructor(device: DeviceWrapper, private messageText: string) { + constructor( + device: DeviceWrapper, + private messageText: string + ) { super(device); } public build(): StrategyExtractionObj { switch (this.platform) { case 'android': - return { - strategy: 'xpath', - selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]` - } as const; + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/layout_emoji_container"]`, + } as const; case 'ios': return { strategy: 'xpath', - selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]` + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]`, } as const; - } } -}; +} export class EmojiReactsCount extends LocatorsInterface { - constructor(device: DeviceWrapper, private messageText: string, private expectedCount: string = '2') { + constructor( + device: DeviceWrapper, + private messageText: string, + private expectedCount: string = '2' + ) { super(device); } @@ -559,12 +565,12 @@ export class EmojiReactsCount extends LocatorsInterface { case 'android': return { strategy: 'xpath', - selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${this.expectedCount}"]` + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger.qa:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger.qa:id/reactions_pill_count"][@text="${this.expectedCount}"]`, } as const; case 'ios': return { strategy: 'xpath', - selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]` + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]`, } as const; } } diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 2939f355a..2e449da58 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -388,7 +388,7 @@ export class BlockedContactsSettings extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Block contacts - Navigation' + selector: 'Block contacts - Navigation', }; } } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2f0e0dbfb..addd7a32c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -329,9 +329,7 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; - const blacklist = [ - { from: 'Voice message', to: 'New voice message'}, - ] + const blacklist = [{ from: 'Voice message', to: 'New voice message' }]; // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores // Strip any known prefix patterns first @@ -378,14 +376,14 @@ export class DeviceWrapper { const selectorConfidence = ((1 - result.score) * 100).toFixed(2); const isBlacklisted = blacklist.some( - pair => - (selector.includes(pair.from) && match.originalSelector.includes(pair.to) || - selector.includes(pair.to) && match.originalSelector.includes(pair.from)) + pair => + (selector.includes(pair.from) && match.originalSelector.includes(pair.to)) || + (selector.includes(pair.to) && match.originalSelector.includes(pair.from)) ); // Don't heal blacklisted pairs if (isBlacklisted) { - continue; + continue; } // Sometimes the element is just not on screen yet - skip @@ -1638,7 +1636,7 @@ export class DeviceWrapper { throw new Error('Send button not found: Need to restart iOS emulator: Known issue'); } // Might need to scroll down if the message is too long - // await this.scrollToBottom(); TODO temporarily disabling this to verify + // await this.scrollToBottom(); TODO temporarily disabling this to verify // Wait for tick await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), diff --git a/run/types/testing.ts b/run/types/testing.ts index 47f808d1f..9507b0008 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -168,7 +168,7 @@ export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().text("Enter your display name")' | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` - | `new UiSelector().text(${string})` + | `new UiSelector().text(${string})`; export type AccessibilityId = | DISAPPEARING_TIMES From 2236fe416a98df64c6dea83dce09b37eb1dae5b5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 10:47:16 +1000 Subject: [PATCH 57/88] fix: clean-up looksSame error messaging --- run/test/specs/utils/verify_screenshots.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 70c458622..1b7b790e1 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -159,8 +159,9 @@ export async function verifyPageScreenshot( if (!equal) { const diffImagePath = await saveImage(diffImage, diffsDir, 'diff'); - throw new Error(`Screenshot did not match baseline. Diff saved to:\n ${diffImagePath}`); - } + console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`) + throw new Error(`The UI doesn't match expected appearance`); + } // Cleanup of element screenshot file on success try { fs.unlinkSync(screenshotName); From 26599e1ac5eb8c4b6a7f42a14da50a429eef0db8 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 11:36:53 +1000 Subject: [PATCH 58/88] refactor: simplify kick member check --- .../specs/group_tests_kick_member.spec.ts | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index df8a9f46b..c6b531de9 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -8,7 +8,6 @@ import { ConfirmRemovalButton, GroupMember, ManageMembersMenuItem, - MemberStatus, RemoveMemberButton, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; @@ -49,20 +48,12 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) .toString() ); await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); - if (platform === 'ios') { - // These elements disappear slowly on iOS so we get a chance to check for their presence - await alice1.waitForTextElementToBePresent(new MemberStatus(alice1).build('Pending removal')); - await alice1.hasElementBeenDeleted({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 10_000, - }); - } else { - // These elements disappear immediately on Android so we can't check for their presence - await alice1.verifyElementNotPresent({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 5_000, - }); - } + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + await alice1.verifyElementNotPresent({ + ...new GroupMember(alice1).build(USERNAME.BOB), + maxWait: 5_000, + }); await alice1.navigateBack(); await alice1.navigateBack(); await Promise.all([ From eba2c80497b83b457582405e914d02d8badd1fa4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 13:45:47 +1000 Subject: [PATCH 59/88] fix: both platforms use the same string now --- run/test/specs/message_requests_decline.spec.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index 9588cfa82..f5f8d3cf6 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -35,17 +35,7 @@ async function declineRequest(platform: SupportedPlatformsType, testInfo: TestIn await device3.waitForTextElementToBePresent(new MessageRequestItem(device3)); // Click on decline button await device2.clickOnElementAll(new DeclineMessageRequestButton(device2)); - // Are you sure you want to delete message request only for ios - await sleepFor(3000); - // TODO remove onIOS/onAndroid once SES-3846 has been completed await device2 - .onIOS() - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString() - ); - await device2 - .onAndroid() .checkModalStrings( englishStrippedStr('delete').toString(), englishStrippedStr('messageRequestsContactDelete').toString() From 04e9ebd1fbef5c33dc8caea2001033d78aaf96ac Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 19 Sep 2025 16:08:28 +1000 Subject: [PATCH 60/88] feat: add global disappearing message timer chore: missed saving a file chore: linting --- run/test/specs/disappear_after_read.spec.ts | 4 +- run/test/specs/disappear_after_send.spec.ts | 3 +- .../specs/disappear_after_send_groups.spec.ts | 3 +- .../disappear_after_send_note_to_self.spec.ts | 3 +- run/test/specs/disappearing_gif.spec.ts | 3 +- run/test/specs/disappearing_image.spec.ts | 31 ++++++------ run/test/specs/disappearing_link.spec.ts | 11 ++++- run/test/specs/disappearing_video.spec.ts | 15 ++++-- run/test/specs/disappearing_voice.spec.ts | 28 +++++------ .../group_disappearing_messages_gif.spec.ts | 5 +- .../group_disappearing_messages_image.spec.ts | 4 +- .../group_disappearing_messages_link.spec.ts | 5 ++ .../group_disappearing_messages_video.spec.ts | 13 +++-- .../group_disappearing_messages_voice.spec.ts | 3 +- run/test/specs/group_message_delete.spec.ts | 3 +- .../specs/group_message_link_preview.spec.ts | 3 +- run/test/specs/group_message_unsend.spec.ts | 3 +- .../specs/group_tests_kick_member.spec.ts | 4 +- .../linked_device_delete_message.spec.ts | 8 ++-- run/test/specs/message_deletion.spec.ts | 3 +- .../specs/message_requests_decline.spec.ts | 9 ++-- run/test/specs/message_unsend.spec.ts | 6 +-- run/test/specs/utils/verify_screenshots.ts | 6 +-- run/types/DeviceWrapper.ts | 48 ++++++++++++++----- 24 files changed, 141 insertions(+), 83 deletions(-) diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index ee22115d6..4f9fc0614 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -53,13 +53,15 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te mode ); // Send message to verify that deletion is working - await alice1.sendMessage(testMessage); + const sentTimestamp = await alice1.sendMessage(testMessage); + // NOTE we're only sending a text message, both devices are open, DaS is practically the same as DaR await Promise.all( [alice1, bob1].map(device => device.hasElementBeenDeleted({ ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index 709cec7de..020519220 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -52,7 +52,7 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te controlMode ); // Send message to verify that deletion is working - await alice1.sendMessage(testMessage); + const sentTimestamp = await alice1.sendMessage(testMessage); // Wait for message to disappear await Promise.all( [alice1, bob1].map(device => @@ -60,6 +60,7 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 3c32a7d36..8ff1ebcae 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -53,13 +53,14 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn charlie1.waitForControlMessageToBePresent(disappearingMessagesSetControl), ]); // Check for test messages (should be deleted) - await alice1.sendMessage(testMessage); + const sentTimestamp = await alice1.sendMessage(testMessage); await Promise.all( [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); 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 dc38c8bea..fb874c07c 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 @@ -48,11 +48,12 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, te await device.waitForControlMessageToBePresent( `You set messages to disappear ${time} after they have been ${controlMode}.` ); - await device.sendMessage(testMessage); + const sentTimestamp = await device.sendMessage(testMessage); await device.hasElementBeenDeleted({ ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }); // Great success await closeApp(device); diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 8d40e74c1..0eb8387f3 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -34,7 +34,7 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.sendGIF(); + const sentTimestamp = await alice1.sendGIF(); await bob1.trustAttachments(USERNAME.ALICE); await Promise.all( [alice1, bob1].map(device => @@ -43,6 +43,7 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI initialMaxWait, maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index d40cf53a8..69c5b6d7e 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -2,9 +2,8 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; -import { MessageBody } from './locators/conversation'; +import { MediaMessage } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; -import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -28,26 +27,24 @@ const maxWait = 35_000; // 30s plus buffer async function disappearingImageMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await sleepFor(500); - await alice1.sendImage(testMessage); - await Promise.all([ - alice1.hasElementBeenDeleted({ - ...new MessageBody(alice1, testMessage).build(), - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', - maxWait, - preventEarlyDeletion: true, - }), - ]); + const sentTimestamp = await alice1.sendImage(testMessage); + await bob1.trustAttachments(alice.userName); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementBeenDeleted({ + ...new MediaMessage(device).build(), + maxWait, + preventEarlyDeletion: true, + actualStartTime: sentTimestamp, + }) + ) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index 3b8b09a40..fbed12164 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -37,6 +37,7 @@ bothPlatformsItSeparate({ const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const timerType = 'Disappear after read option'; const maxWait = 35_000; // 30s plus buffer +let sentTimestamp: number; async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, testInfo: TestInfo) { const { @@ -71,6 +72,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); // Wait for 30 seconds to disappear await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { @@ -80,6 +82,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t ...new MessageBody(device, testLink).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -122,11 +125,17 @@ async function disappearingLinkMessage1o1Android( ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ ...new LinkPreviewMessage(device).build(), maxWait }) + device.hasElementBeenDeleted({ + ...new LinkPreviewMessage(device).build(), + maxWait, + preventEarlyDeletion: true, + actualStartTime: sentTimestamp, + }) ) ); }); diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9d4e4f62a..b52ae4527 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; +import { DISAPPEARING_TIMES } from '../../types/testing'; import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -24,7 +24,7 @@ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const testMessage = 'Testing disappearing messages for videos'; const initialMaxWait = 20_000; // Downloading the attachment can take a while -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { @@ -35,9 +35,12 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.onIOS().sendVideoiOS(testMessage); - await alice1.onAndroid().sendVideoAndroid(); - await bob1.trustAttachments(USERNAME.ALICE); + let sentTimestamp: number; + if (platform === 'ios') { + sentTimestamp = await alice1.onIOS().sendVideoiOS(testMessage); + } else { + sentTimestamp = await alice1.onAndroid().sendVideoAndroid(); + } if (platform === 'ios') { await Promise.all( [alice1, bob1].map(device => @@ -46,6 +49,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes initialMaxWait, maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -57,6 +61,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes initialMaxWait, maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index 3285cac5c..aadced248 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -26,26 +26,24 @@ const maxWait = 35_000; // 30s plus buffer async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - await alice1.sendVoiceMessage(); - await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); - await Promise.all([ - alice1.hasElementBeenDeleted({ - ...new VoiceMessage(alice1).build(), - maxWait, - preventEarlyDeletion: true, - }), - bob1.hasElementBeenDeleted({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', - maxWait, - preventEarlyDeletion: true, - }), - ]); + const sentTimestamp = await alice1.sendVoiceMessage(); + await bob1.trustAttachments(alice.userName); + await Promise.all( + [alice1, bob1].map(device => + device.hasElementBeenDeleted({ + ...new VoiceMessage(device).build(), + maxWait, + preventEarlyDeletion: true, + actualStartTime: sentTimestamp, + }) + ) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index d97f91a5b..4b51133a8 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -37,7 +37,8 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); // Click on attachments button - await alice1.sendGIF(); + const sentTimestamp = await alice1.sendGIF(); + console.log(`the sent timestamp is ${sentTimestamp}`); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); @@ -45,10 +46,10 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ ...new MediaMessage(device).build(), - initialMaxWait, maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 2081defa8..ffe8df510 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -35,7 +35,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - await alice1.sendImage(testMessage); + const sentTimestamp = await alice1.sendImage(testMessage); if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => @@ -43,6 +43,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -61,6 +62,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t selector: 'Untrusted attachment message', maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ), ]); diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index 364008b14..8cb4e5083 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -33,6 +33,7 @@ const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + let sentTimestamp: number; const testGroupName = 'Testing disappearing messages'; const { devices: { alice1, bob1, charlie1 }, @@ -68,6 +69,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); + sentTimestamp = Date.now(); }); // Wait for 30 seconds to disappear await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { @@ -78,6 +80,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te ...new MessageBody(device, testLink).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -88,6 +91,8 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te device.hasElementBeenDeleted({ ...new LinkPreviewMessage(device).build(), maxWait, + preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 54b67b64b..fbd2aa669 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -23,7 +23,7 @@ bothPlatformsIt({ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const initialMaxWait = 20_000; // Downloading the attachment can take a while -const maxWait = 65_000; // 60s plus buffer +const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testMessage = 'Testing disappearing messages for videos'; @@ -37,8 +37,12 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t testInfo, }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - await alice1.onIOS().sendVideoiOS(testMessage); - await alice1.onAndroid().sendVideoAndroid(); + let sentTimestamp: number; + if (platform === 'ios') { + sentTimestamp = await alice1.sendVideoiOS(testMessage); + } else { + sentTimestamp = await alice1.sendVideoAndroid(); + } await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); @@ -49,6 +53,7 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t ...new MessageBody(device, testMessage).build(), maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); @@ -57,10 +62,10 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t [alice1, bob1, charlie1].map(device => device.hasElementBeenDeleted({ ...new MediaMessage(device).build(), - initialMaxWait, maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_disappearing_messages_voice.spec.ts b/run/test/specs/group_disappearing_messages_voice.spec.ts index dd7572f4d..cb0dea913 100644 --- a/run/test/specs/group_disappearing_messages_voice.spec.ts +++ b/run/test/specs/group_disappearing_messages_voice.spec.ts @@ -32,7 +32,7 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t testInfo, }); await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); - await alice1.sendVoiceMessage(); + const sentTimestamp = await alice1.sendVoiceMessage(); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); @@ -43,6 +43,7 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t selector: 'Voice message', maxWait, preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }) ) ); diff --git a/run/test/specs/group_message_delete.spec.ts b/run/test/specs/group_message_delete.spec.ts index e59be0fd5..c3fc493c9 100644 --- a/run/test/specs/group_message_delete.spec.ts +++ b/run/test/specs/group_message_delete.spec.ts @@ -24,7 +24,8 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te focusGroupConvo: true, testInfo, }); - const sentMessage = await alice1.sendMessage('Checking local delete functionality'); + const sentMessage = 'Checking local delete functionality '; + await alice1.sendMessage(sentMessage); await Promise.all( [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) diff --git a/run/test/specs/group_message_link_preview.spec.ts b/run/test/specs/group_message_link_preview.spec.ts index 3abee373b..f3b73a5e1 100644 --- a/run/test/specs/group_message_link_preview.spec.ts +++ b/run/test/specs/group_message_link_preview.spec.ts @@ -108,7 +108,8 @@ async function sendLinkGroupAndroid(platform: SupportedPlatformsType, testInfo: ]); await bob1.longPressMessage(testLink); await bob1.clickOnByAccessibilityID('Reply to message'); - const replyMessage = await bob1.sendMessage(`${alice.userName} message reply`); + const replyMessage = `${alice.userName} message reply`; + await bob1.sendMessage(replyMessage); await Promise.all( [alice1, charlie1].map(device => device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)) diff --git a/run/test/specs/group_message_unsend.spec.ts b/run/test/specs/group_message_unsend.spec.ts index 12384b49a..3ecd759fa 100644 --- a/run/test/specs/group_message_unsend.spec.ts +++ b/run/test/specs/group_message_unsend.spec.ts @@ -25,7 +25,8 @@ async function unsendMessageGroup(platform: SupportedPlatformsType, testInfo: Te focusGroupConvo: true, testInfo, }); - const sentMessage = await alice1.sendMessage('Checking unsend functionality'); + const sentMessage = 'Checking unsend functionality'; + await alice1.sendMessage(sentMessage); await Promise.all( [bob1, charlie1].map(device => device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index c6b531de9..3c3a1555a 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -48,8 +48,8 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) .toString() ); await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); - // The Group Member element sometimes disappears slowly, sometimes quickly. - // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore await alice1.verifyElementNotPresent({ ...new GroupMember(alice1).build(USERNAME.BOB), maxWait: 5_000, diff --git a/run/test/specs/linked_device_delete_message.spec.ts b/run/test/specs/linked_device_delete_message.spec.ts index 97f35cf82..1f21683f6 100644 --- a/run/test/specs/linked_device_delete_message.spec.ts +++ b/run/test/specs/linked_device_delete_message.spec.ts @@ -22,13 +22,13 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test const testMessage = 'Howdy'; // Send message from user a to user b - const sentMessage = await alice1.sendMessage(testMessage); + await alice1.sendMessage(testMessage); // Check message came through on linked device(3) // Enter conversation with user B on device 3 await alice2.clickOnElementAll(new ConversationItem(alice2, bob.userName)); // Find message - await alice2.findMessageWithBody(sentMessage); + await alice2.findMessageWithBody(testMessage); // Select message on device 1, long press - await alice1.longPressMessage(sentMessage); + await alice1.longPressMessage(testMessage); // Select delete await alice1.clickOnByAccessibilityID('Delete message'); await alice1.checkModalStrings( @@ -41,7 +41,7 @@ async function deletedMessageLinkedDevice(platform: SupportedPlatformsType, test // Check device 2 and 3 for no change await Promise.all( [bob1, alice2].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, sentMessage)) + device.waitForTextElementToBePresent(new MessageBody(device, testMessage)) ) ); // Close app diff --git a/run/test/specs/message_deletion.spec.ts b/run/test/specs/message_deletion.spec.ts index 76c3059f3..5297b8740 100644 --- a/run/test/specs/message_deletion.spec.ts +++ b/run/test/specs/message_deletion.spec.ts @@ -22,7 +22,8 @@ async function deleteMessage(platform: SupportedPlatformsType, testInfo: TestInf testInfo, }); // send message from User A to User B - const sentMessage = await alice1.sendMessage('Checking local deletetion functionality'); + const sentMessage = 'Checking local deletetion functionality'; + await alice1.sendMessage(sentMessage); await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); // Select and long press on message to delete it await alice1.longPressMessage(sentMessage); diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index f5f8d3cf6..442a6032d 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -35,11 +35,10 @@ async function declineRequest(platform: SupportedPlatformsType, testInfo: TestIn await device3.waitForTextElementToBePresent(new MessageRequestItem(device3)); // Click on decline button await device2.clickOnElementAll(new DeclineMessageRequestButton(device2)); - await device2 - .checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString() - ); + await device2.checkModalStrings( + englishStrippedStr('delete').toString(), + englishStrippedStr('messageRequestsContactDelete').toString() + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); diff --git a/run/test/specs/message_unsend.spec.ts b/run/test/specs/message_unsend.spec.ts index 779d9ba40..8b90fb94a 100644 --- a/run/test/specs/message_unsend.spec.ts +++ b/run/test/specs/message_unsend.spec.ts @@ -25,10 +25,10 @@ async function unsendMessage(platform: SupportedPlatformsType, testInfo: TestInf const testMessage = 'Checking unsend functionality'; // send message from User A to User B - const sentMessage = await alice1.sendMessage(testMessage); + await alice1.sendMessage(testMessage); // await sleepFor(1000); - await bob1.waitForTextElementToBePresent(new MessageBody(bob1, sentMessage)); - await alice1.longPressMessage(sentMessage); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testMessage)); + await alice1.longPressMessage(testMessage); // Select Delete icon await alice1.clickOnByAccessibilityID('Delete message'); // Check modal is correct diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 1b7b790e1..9b3cb3e9b 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -159,9 +159,9 @@ export async function verifyPageScreenshot( if (!equal) { const diffImagePath = await saveImage(diffImage, diffsDir, 'diff'); - console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`) - throw new Error(`The UI doesn't match expected appearance`); - } + console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`); + throw new Error(`The UI doesn't match expected appearance`); + } // Cleanup of element screenshot file on success try { fs.unlinkSync(screenshotName); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index addd7a32c..47b5d1af5 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1203,6 +1203,7 @@ export class DeviceWrapper { * @param args.initialMaxWait - Time to wait for element to initially appear (defaults to 10_000ms) * @param args.maxWait - Time to wait for deletion AFTER element is found (defaults to 30_000ms) * @param args.preventEarlyDeletion - If true, throws an error if the element disappears too early (% of maxWait) + * @param args.actualStartTime - Optional timestamp of when the timer should be considered to have started. If provided, total time is calculated from this timestamp rather than when this method is called. Useful for disappearing messages where the timer starts at send time. * * @throws Error if: * - The element is never found within initialMaxWait @@ -1217,6 +1218,7 @@ export class DeviceWrapper { initialMaxWait?: number; maxWait?: number; preventEarlyDeletion?: boolean; + actualStartTime?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1227,7 +1229,7 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text: args.text }); // Track total time from start - disappearing timers begin on send, not on display - const functionStartTime = Date.now(); + const functionStartTime = args.actualStartTime ?? Date.now(); // Phase 1: Wait for element to appear this.log(`Waiting for element with ${description} to be deleted...`); await this.waitForElementToAppear(locator, initialMaxWait, text); @@ -1626,7 +1628,7 @@ export class DeviceWrapper { } // UTILITY FUNCTIONS - public async sendMessage(message: string) { + public async sendMessage(message: string): Promise { await this.inputText(message, new MessageInput(this)); // Click send @@ -1642,8 +1644,8 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 50000, }); - - return message; + const sentTimestamp = Date.now(); + return sentTimestamp; } public async waitForSentConfirmation() { @@ -1717,7 +1719,8 @@ export class DeviceWrapper { } // Select 'Reply' option // Send message - const replyMessage = await this.sendMessage(`${user.userName} replied to ${body}`); + const replyMessage = `${user.userName} replied to ${body}`; + await this.sendMessage(replyMessage); return replyMessage; } @@ -1811,7 +1814,7 @@ export class DeviceWrapper { } } - public async sendImage(message: string, community?: boolean) { + public async sendImage(message: string, community?: boolean): Promise { if (this.isIOS()) { // Push file first await this.pushMediaToDevice(testImage); @@ -1861,8 +1864,10 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVideoiOS(message: string) { + public async sendVideoiOS(message: string): Promise { // Push first await this.pushMediaToDevice(testVideo); await this.clickOnElementAll(new AttachmentsButton(this)); @@ -1894,9 +1899,11 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVideoAndroid() { + public async sendVideoAndroid(): Promise { // Push first await this.pushMediaToDevice(testVideo); // Click on attachments button @@ -1950,9 +1957,12 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + this.log(`[DEBUG] sendVideoiOS returning timestamp: ${sentTimestamp}`); + return sentTimestamp; } - public async sendDocument() { + public async sendDocument(): Promise { if (this.isIOS()) { const formattedFileName = 'test_file, pdf'; const testMessage = 'Testing documents'; @@ -2039,9 +2049,11 @@ export class DeviceWrapper { ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendGIF() { + public async sendGIF(): Promise { await sleepFor(1000); await this.clickOnByAccessibilityID('Attachments button'); if (this.isAndroid()) { @@ -2064,9 +2076,16 @@ export class DeviceWrapper { if (this.isIOS()) { await this.clickOnElementAll(new SendButton(this)); } + // Checking Sent status on both platforms + await this.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(this).build(), + maxWait: 20000, + }); + const sentTimestamp = Date.now(); + return sentTimestamp; } - public async sendVoiceMessage() { + public async sendVoiceMessage(): Promise { await this.longPress(new NewVoiceMessageButton(this)); if (this.isAndroid()) { @@ -2081,6 +2100,13 @@ export class DeviceWrapper { } await this.pressAndHold('New voice message'); + // Checking Sent status on both platforms + await this.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(this).build(), + maxWait: 20000, + }); + const sentTimestamp = Date.now(); + return sentTimestamp; } public async uploadProfilePicture() { From b8d816c203beccfafed7a2774940c12a87881187 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 10:18:39 +1000 Subject: [PATCH 61/88] feat: add separate hasElementDisappeared for DM logic --- run/test/specs/disappear_after_read.spec.ts | 3 +- run/test/specs/disappear_after_send.spec.ts | 3 +- .../specs/disappear_after_send_groups.spec.ts | 3 +- .../disappear_after_send_note_to_self.spec.ts | 3 +- run/test/specs/disappearing_call.spec.ts | 9 +- .../disappearing_community_invite.spec.ts | 5 +- run/test/specs/disappearing_gif.spec.ts | 3 +- run/test/specs/disappearing_image.spec.ts | 3 +- run/test/specs/disappearing_link.spec.ts | 7 +- run/test/specs/disappearing_video.spec.ts | 6 +- run/test/specs/disappearing_voice.spec.ts | 3 +- .../group_disappearing_messages_gif.spec.ts | 3 +- .../group_disappearing_messages_image.spec.ts | 10 +- .../group_disappearing_messages_link.spec.ts | 6 +- .../group_disappearing_messages_video.spec.ts | 6 +- .../group_disappearing_messages_voice.spec.ts | 3 +- run/test/specs/group_message_delete.spec.ts | 2 +- run/types/DeviceWrapper.ts | 95 +++++++++++++------ 18 files changed, 96 insertions(+), 77 deletions(-) diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index 4f9fc0614..47f0db209 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -57,10 +57,9 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te // NOTE we're only sending a text message, both devices are open, DaS is practically the same as DaR await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index 020519220..b19ffb202 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -56,10 +56,9 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te // Wait for message to disappear await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 8ff1ebcae..7399d8945 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -56,10 +56,9 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn const sentTimestamp = await alice1.sendMessage(testMessage); await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) 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 fb874c07c..43d7e0bff 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 @@ -49,10 +49,9 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, te `You set messages to disappear ${time} after they have been ${controlMode}.` ); const sentTimestamp = await device.sendMessage(testMessage); - await device.hasElementBeenDeleted({ + await device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }); // Great success diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 103e77170..1ce067e4b 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -144,21 +144,22 @@ async function disappearingCallMessage1o1Android( maxWait: 5_000, }); await alice1.clickOnElementById('network.loki.messenger.qa:id/endCallButton'); + const callEndTimestamp = Date.now(); // Wait for control message to disappear await Promise.all([ - alice1.hasElementBeenDeleted({ + alice1.hasElementDisappeared({ strategy: 'id', selector: 'network.loki.messenger.qa:id/call_text_view', text: `You called ${bob.userName}`, maxWait, - preventEarlyDeletion: true, + actualStartTime: callEndTimestamp, }), - bob1.hasElementBeenDeleted({ + bob1.hasElementDisappeared({ strategy: 'id', selector: 'network.loki.messenger.qa:id/call_text_view', text: `Missed call from ${alice.userName}`, maxWait, - preventEarlyDeletion: true, + actualStartTime: callEndTimestamp, }), ]); await closeApp(alice1, bob1); diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 12890a2cb..8bf16c480 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -58,11 +58,12 @@ async function disappearingCommunityInviteMessage( await alice1.clickOnElementAll(new CommunityInviteConfirmButton(alice1)); // The community invite process fails silently so we will check if the invite came through first await bob1.waitForTextElementToBePresent(new CommunityInvitation(bob1)); + const communityInviteTimestamp = Date.now(); // Bob already has the convo open so we can start checking for the disappearing message immediately - await bob1.hasElementBeenDeleted({ + await bob1.hasElementDisappeared({ ...new CommunityInvitation(bob1).build(), maxWait, - preventEarlyDeletion: true, + actualStartTime: communityInviteTimestamp, }); // Leave Invite Contacts, Conversation Settings, Community, and open convo with Bob await alice1.navigateBack(); diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 0eb8387f3..657f08802 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -38,11 +38,10 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI await bob1.trustAttachments(USERNAME.ALICE); await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index 69c5b6d7e..3f2e826ed 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -38,10 +38,9 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes await bob1.trustAttachments(alice.userName); await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MediaMessage(device).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index fbed12164..ccd653ff1 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -66,7 +66,6 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t await alice1.deleteText(new MessageInput(alice1)); await alice1.inputText(testLink, new MessageInput(alice1)); await alice1.waitForTextElementToBePresent(new LinkPreview(alice1)); - await alice1.clickOnElementAll(new SendButton(alice1)); await alice1.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(alice1).build(), @@ -78,10 +77,9 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testLink).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) @@ -130,10 +128,9 @@ async function disappearingLinkMessage1o1Android( await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new LinkPreviewMessage(device).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index b52ae4527..9dd751fe7 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -44,11 +44,10 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes if (platform === 'ios') { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) @@ -56,11 +55,10 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes } else if (platform === 'android') { await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index aadced248..54713a713 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -37,10 +37,9 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes await bob1.trustAttachments(alice.userName); await Promise.all( [alice1, bob1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new VoiceMessage(device).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index 4b51133a8..c5af419ea 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -44,11 +44,10 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes ); await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index ffe8df510..5dea27a3f 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -39,10 +39,9 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) @@ -50,18 +49,17 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t } if (platform === 'android') { await Promise.all([ - alice1.hasElementBeenDeleted({ + alice1.hasElementDisappeared({ ...new MediaMessage(alice1).build(), maxWait, - preventEarlyDeletion: true, + actualStartTime: sentTimestamp, }), // Bob and Charlie haven't trusted the message ...[bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ strategy: 'accessibility id', selector: 'Untrusted attachment message', maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ), diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index 8cb4e5083..c163f8501 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -76,10 +76,9 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testLink).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) @@ -88,10 +87,9 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te if (platform === 'android') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new LinkPreviewMessage(device).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index fbd2aa669..1ae36d83e 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -49,10 +49,9 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t if (platform === 'ios') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MessageBody(device, testMessage).build(), maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) @@ -60,11 +59,10 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t } else if (platform === 'android') { await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ ...new MediaMessage(device).build(), initialMaxWait, maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/group_disappearing_messages_voice.spec.ts b/run/test/specs/group_disappearing_messages_voice.spec.ts index cb0dea913..83c1bf6a0 100644 --- a/run/test/specs/group_disappearing_messages_voice.spec.ts +++ b/run/test/specs/group_disappearing_messages_voice.spec.ts @@ -38,11 +38,10 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t ); await Promise.all( [alice1, bob1, charlie1].map(device => - device.hasElementBeenDeleted({ + device.hasElementDisappeared({ strategy: 'accessibility id', selector: 'Voice message', maxWait, - preventEarlyDeletion: true, actualStartTime: sentTimestamp, }) ) diff --git a/run/test/specs/group_message_delete.spec.ts b/run/test/specs/group_message_delete.spec.ts index c3fc493c9..9d80bfd3a 100644 --- a/run/test/specs/group_message_delete.spec.ts +++ b/run/test/specs/group_message_delete.spec.ts @@ -24,7 +24,7 @@ async function deleteMessageGroup(platform: SupportedPlatformsType, testInfo: Te focusGroupConvo: true, testInfo, }); - const sentMessage = 'Checking local delete functionality '; + const sentMessage = 'Checking local delete functionality'; await alice1.sendMessage(sentMessage); await Promise.all( [bob1, charlie1].map(device => diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 47b5d1af5..b471ba2fa 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -329,7 +329,10 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; - const blacklist = [{ from: 'Voice message', to: 'New voice message' }]; + const blacklist = [ + { from: 'Voice message', to: 'New voice message' }, + { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, + ]; // System locators such as 'network.loki.messenger.qa:id' can cause false positives with too high similarity scores // Strip any known prefix patterns first @@ -1202,23 +1205,16 @@ export class DeviceWrapper { * @param args.text - Optional text content to match within elements of the same type * @param args.initialMaxWait - Time to wait for element to initially appear (defaults to 10_000ms) * @param args.maxWait - Time to wait for deletion AFTER element is found (defaults to 30_000ms) - * @param args.preventEarlyDeletion - If true, throws an error if the element disappears too early (% of maxWait) - * @param args.actualStartTime - Optional timestamp of when the timer should be considered to have started. If provided, total time is calculated from this timestamp rather than when this method is called. Useful for disappearing messages where the timer starts at send time. * * @throws Error if: * - The element is never found within initialMaxWait * - The element still exists after maxWait - * - The element disappears suspiciously early (if preventEarlyDeletion is true) - * Note: For checks where you just need to ensure an element - * is not present (regardless of prior existence), use verifyElementNotPresent() instead. */ public async hasElementBeenDeleted( args: { text?: string; initialMaxWait?: number; maxWait?: number; - preventEarlyDeletion?: boolean; - actualStartTime?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1229,11 +1225,10 @@ export class DeviceWrapper { const description = describeLocator({ ...locator, text: args.text }); // Track total time from start - disappearing timers begin on send, not on display - const functionStartTime = args.actualStartTime ?? Date.now(); + const functionStartTime = Date.now(); // Phase 1: Wait for element to appear this.log(`Waiting for element with ${description} to be deleted...`); await this.waitForElementToAppear(locator, initialMaxWait, text); - const foundTime = Date.now(); this.log(`Element with ${description} has been found, now waiting for deletion`); // Phase 2: Wait for element to disappear @@ -1242,21 +1237,67 @@ export class DeviceWrapper { // Always calculate total time for logging const totalTime = (Date.now() - functionStartTime) / 1000; - // Sometimes an early deletion could indicate a bug (e.g. Disappearing Messages) - if (args.preventEarlyDeletion) { - const deletionPhaseTime = (Date.now() - foundTime) / 1000; - const expectedTotalTime = maxWait / 1000; - const minAcceptableTotalTimeFactor = 0.65; // Catches egregiously early deletions but still enough leeway for sending/trusting/receiving - const minAcceptableTotalTime = expectedTotalTime * minAcceptableTotalTimeFactor; - - if (totalTime < minAcceptableTotalTime) { - throw new Error( - `Element with ${description} disappeared suspiciously early: ${totalTime.toFixed(1)}s total ` + - `(found after ${((foundTime - functionStartTime) / 1000).toFixed(1)}s, ` + - `deleted after ${deletionPhaseTime.toFixed(1)}s). ` + - `Expected ~${expectedTotalTime}s total.` - ); - } + this.log( + `Element with ${description} has been deleted after ${totalTime.toFixed(1)}s total time` + ); + } + + /** + * Waits for an element to disappear from screen (using the Disappearing Messages feature) + * + * @param args - Locator (LocatorsInterface or StrategyExtractionObj) with optional properties + * @param args.actualStartTime - Timestamp of when the timer should be considered to have started. + * @param args.text - Optional text content to match within elements of the same type + * @param args.initialMaxWait - Time to wait for element to initially appear (defaults to 10_000ms) + * @param args.maxWait - Time to wait for deletion AFTER element is found (defaults to 30_000ms) + * + * @throws Error if: + * - The element is never found within initialMaxWait + * - The element still exists after maxWait + * - The element disappears suspiciously early + * + * Note: + * - If you want to ensure an element was present but disappeared (without Disappearing Messages logic), use hasElementBeenDeleted(). + * - If you want to ensure an element is no longer visible (regardless of prior existence), use verifyElementNotPresent(). + */ + public async hasElementDisappeared( + args: { + actualStartTime: number; + text?: string; + initialMaxWait?: number; + maxWait?: number; + } & (LocatorsInterface | StrategyExtractionObj) + ): Promise { + const locator = args instanceof LocatorsInterface ? args.build() : args; + const text = args.text; + const initialMaxWait = args.initialMaxWait ?? 10_000; + const maxWait = args.maxWait ?? 30_000; + + const description = describeLocator({ ...locator, text: args.text }); + + // Phase 1: Wait for element to appear + this.log(`Waiting for element with ${description} to be deleted...`); + await this.waitForElementToAppear(locator, initialMaxWait, text); + const foundTime = Date.now(); + this.log(`Element with ${description} has been found, now waiting for deletion`); + + // Phase 2: Wait for element to disappear + await this.waitForElementToDisappear(locator, maxWait, text); + + // Elements should not disappear too early (could be a DM bug) + const totalTime = (Date.now() - args.actualStartTime) / 1000; + const deletionPhaseTime = (Date.now() - foundTime) / 1000; + const expectedTotalTime = maxWait / 1000; + const minAcceptableTotalTimeFactor = 0.65; // Catches egregiously early deletions but still enough leeway for sending/trusting/receiving + const minAcceptableTotalTime = expectedTotalTime * minAcceptableTotalTimeFactor; + + if (totalTime < minAcceptableTotalTime) { + throw new Error( + `Element with ${description} disappeared suspiciously early: ${totalTime.toFixed(1)}s total ` + + `(found after ${((foundTime - args.actualStartTime) / 1000).toFixed(1)}s, ` + + `deleted after ${deletionPhaseTime.toFixed(1)}s). ` + + `Expected ~${expectedTotalTime}s total.` + ); } this.log( @@ -2187,10 +2228,6 @@ export class DeviceWrapper { } public async trustAttachments(conversationName: string) { - await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', - }); await this.clickOnElementAll({ strategy: 'accessibility id', selector: 'Untrusted attachment message', From dc07a9bfec1b5ef0fed77bf3d1a4bcf001e07a6e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 13:42:22 +1000 Subject: [PATCH 62/88] fix: trust attachments --- run/test/specs/disappearing_video.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9dd751fe7..9bacd8cde 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -29,6 +29,7 @@ const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, + prebuilt: { alice } } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, @@ -41,6 +42,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes } else { sentTimestamp = await alice1.onAndroid().sendVideoAndroid(); } + await bob1.trustAttachments(alice.userName) if (platform === 'ios') { await Promise.all( [alice1, bob1].map(device => From 548c887b9a2fbe38cdf78de07ea539f0f3a8fb4b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 13:42:30 +1000 Subject: [PATCH 63/88] fix: correct locator for photos app --- run/test/specs/utils/handle_first_open.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/specs/utils/handle_first_open.ts b/run/test/specs/utils/handle_first_open.ts index 46cf09be5..d9fe88ff0 100644 --- a/run/test/specs/utils/handle_first_open.ts +++ b/run/test/specs/utils/handle_first_open.ts @@ -37,8 +37,8 @@ export async function handlePhotosFirstTimeOpen(device: DeviceWrapper) { // On Android, the Photos app shows a sign-in prompt the first time it's opened that needs to be dismissed if (device.isAndroid()) { const signInButton = await device.doesElementExist({ - strategy: 'id', - selector: 'com.google.android.apps.photos:id/sign_in_button', + strategy: '-android uiautomator', + selector: 'new UiSelector().text("Sign in")', maxWait: 2_000, }); if (!signInButton) { From c48d7de709c91551c922e7af05f10d2bf945d6e6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 14:16:44 +1000 Subject: [PATCH 64/88] fix: waitForElementToAppear uses pollUntil --- run/types/DeviceWrapper.ts | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index b471ba2fa..068023bcb 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1307,24 +1307,32 @@ export class DeviceWrapper { /** * Wait for an element to appear on screen */ - private async waitForElementToAppear( - locator: StrategyExtractionObj, - timeout: number, - text?: string - ): Promise { - const start = Date.now(); - - while (Date.now() - start < timeout) { - const element = await this.findElementQuietly(locator, text); - if (element) return; - await sleepFor(100); +private async waitForElementToAppear( + locator: StrategyExtractionObj, + timeout: number, + text?: string +): Promise { + const desc = describeLocator({ ...locator, text }); + + const element = await this.pollUntil( + async () => { + const foundElement = await this.findElementQuietly(locator, text); + return foundElement + ? { success: true, data: foundElement } + : { success: false, error: `Element with ${desc} not found` }; + }, + { + maxWait: timeout, + pollInterval: 100, } + ); - const desc = describeLocator({ ...locator, text }); + if (!element) { throw new Error( `Element with ${desc} was never found within ${timeout}ms - cannot verify deletion of non-existent element` ); } +} /** * Wait for an element to disappear with debouncing for flaky UI states. From e3404d735b614ec23b28680b7c1d56be993926cf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 14:16:58 +1000 Subject: [PATCH 65/88] fix: media message not identifiable on ios --- run/test/specs/disappearing_image.spec.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index 3f2e826ed..91de0c854 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; -import { MediaMessage } from './locators/conversation'; +import { MediaMessage, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -36,6 +36,17 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); const sentTimestamp = await alice1.sendImage(testMessage); await bob1.trustAttachments(alice.userName); + if (platform === 'ios') { + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + } else { await Promise.all( [alice1, bob1].map(device => device.hasElementDisappeared({ @@ -45,5 +56,6 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes }) ) ); +} await closeApp(alice1, bob1); } From 880e46168d0fb47997d3abc9cf18ee3a0eb8f715 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 14:17:06 +1000 Subject: [PATCH 66/88] chore: linting --- run/test/specs/disappearing_image.spec.ts | 36 +++++++++--------- run/test/specs/disappearing_video.spec.ts | 4 +- run/types/DeviceWrapper.ts | 46 +++++++++++------------ 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index 91de0c854..e4d98645c 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -38,24 +38,24 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes await bob1.trustAttachments(alice.userName); if (platform === 'ios') { await Promise.all( - [alice1, bob1].map(device => - device.hasElementDisappeared({ - ...new MessageBody(device, testMessage).build(), - maxWait, - actualStartTime: sentTimestamp, - }) - ) - ); + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); } else { - await Promise.all( - [alice1, bob1].map(device => - device.hasElementDisappeared({ - ...new MediaMessage(device).build(), - maxWait, - actualStartTime: sentTimestamp, - }) - ) - ); -} + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MediaMessage(device).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + } await closeApp(alice1, bob1); } diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9bacd8cde..67df3bb6f 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -29,7 +29,7 @@ const maxWait = 70_000; // 60s plus buffer async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, - prebuilt: { alice } + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, @@ -42,7 +42,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes } else { sentTimestamp = await alice1.onAndroid().sendVideoAndroid(); } - await bob1.trustAttachments(alice.userName) + await bob1.trustAttachments(alice.userName); if (platform === 'ios') { await Promise.all( [alice1, bob1].map(device => diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 068023bcb..499ec2062 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1307,32 +1307,32 @@ export class DeviceWrapper { /** * Wait for an element to appear on screen */ -private async waitForElementToAppear( - locator: StrategyExtractionObj, - timeout: number, - text?: string -): Promise { - const desc = describeLocator({ ...locator, text }); - - const element = await this.pollUntil( - async () => { - const foundElement = await this.findElementQuietly(locator, text); - return foundElement - ? { success: true, data: foundElement } - : { success: false, error: `Element with ${desc} not found` }; - }, - { - maxWait: timeout, - pollInterval: 100, - } - ); + private async waitForElementToAppear( + locator: StrategyExtractionObj, + timeout: number, + text?: string + ): Promise { + const desc = describeLocator({ ...locator, text }); - if (!element) { - throw new Error( - `Element with ${desc} was never found within ${timeout}ms - cannot verify deletion of non-existent element` + const element = await this.pollUntil( + async () => { + const foundElement = await this.findElementQuietly(locator, text); + return foundElement + ? { success: true, data: foundElement } + : { success: false, error: `Element with ${desc} not found` }; + }, + { + maxWait: timeout, + pollInterval: 100, + } ); + + if (!element) { + throw new Error( + `Element with ${desc} was never found within ${timeout}ms - cannot verify deletion of non-existent element` + ); + } } -} /** * Wait for an element to disappear with debouncing for flaky UI states. From 6a503bda4640fbcc7a926cbd5bd8799c8938412c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 22 Sep 2025 15:52:12 +1000 Subject: [PATCH 67/88] fix: compare user avatar color without refreshing settings page --- run/test/specs/linked_device_profile_picture_syncs.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/run/test/specs/linked_device_profile_picture_syncs.spec.ts b/run/test/specs/linked_device_profile_picture_syncs.spec.ts index abd561abb..447da819e 100644 --- a/run/test/specs/linked_device_profile_picture_syncs.spec.ts +++ b/run/test/specs/linked_device_profile_picture_syncs.spec.ts @@ -2,7 +2,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { UserSettings } from './locators/settings'; +import { UserAvatar, UserSettings } from './locators/settings'; import { open_Alice2 } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -24,10 +24,11 @@ async function avatarRestored(platform: SupportedPlatformsType, testInfo: TestIn const { devices: { alice1, alice2 }, } = await open_Alice2({ platform, testInfo }); + await alice2.clickOnElementAll(new UserSettings(alice2)); await alice1.uploadProfilePicture(); await test.step(TestSteps.VERIFY.PROFILE_PICTURE_CHANGED, async () => { await alice2.waitForElementColorMatch( - { ...new UserSettings(alice2).build(), maxWait: 20_000 }, + { ...new UserAvatar(alice2).build(), maxWait: 20_000 }, expectedPixelHexColor ); }); From eb75db2b67657082475713c373e535d786118fdf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 23 Sep 2025 11:06:23 +1000 Subject: [PATCH 68/88] fix: add display name id --- run/test/specs/locators/index.ts | 2 +- run/types/testing.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 2e449da58..a6b84ab92 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -112,7 +112,7 @@ export class UsernameDisplay extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'pro-badge-text', + selector: 'Display name', text: this.text, } as const; case 'ios': diff --git a/run/types/testing.ts b/run/types/testing.ts index 9507b0008..e634fc656 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -467,6 +467,7 @@ export type Id = | 'Disable disappearing messages' | 'disappearing-messages-menu-option' | 'Disappearing messages type and time' + | 'Display name' | 'donate-menu-item' | 'Download media' | 'edit-profile-icon' From d2587409684d7c9328c2b59790ba337a5060cb9b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 23 Sep 2025 11:07:17 +1000 Subject: [PATCH 69/88] fix: photos app nonsense --- run/test/specs/utils/handle_first_open.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/run/test/specs/utils/handle_first_open.ts b/run/test/specs/utils/handle_first_open.ts index d9fe88ff0..3844220b7 100644 --- a/run/test/specs/utils/handle_first_open.ts +++ b/run/test/specs/utils/handle_first_open.ts @@ -35,12 +35,22 @@ export async function handlePhotosFirstTimeOpen(device: DeviceWrapper) { } } // On Android, the Photos app shows a sign-in prompt the first time it's opened that needs to be dismissed + // I've seen two different kinds of sign in buttons on the same set of emulators if (device.isAndroid()) { - const signInButton = await device.doesElementExist({ - strategy: '-android uiautomator', - selector: 'new UiSelector().text("Sign in")', - maxWait: 2_000, + let signInButton = null; + signInButton = await device.doesElementExist({ + strategy: 'id', + selector: 'com.google.android.apps.photos:id/sign_in_button', + maxWait: 1_000, }); + + if (!signInButton) { + signInButton = await device.doesElementExist({ + strategy: '-android uiautomator', + selector: 'new UiSelector().text("Sign in")', + maxWait: 1_000, + }); + } if (!signInButton) { device.log(`Photos app opened without a sign-in prompt, proceeding`); } else { From 212fa37bbad3209e320714b171e9fed117b48ac2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 24 Sep 2025 16:59:34 +1000 Subject: [PATCH 70/88] feat: keep adding test.steps --- run/test/specs/community_tests_join.spec.ts | 31 +++++--- run/test/specs/disappear_after_read.spec.ts | 78 +++++++++++-------- .../group_disappearing_messages_link.spec.ts | 2 +- run/types/allure.ts | 4 +- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index 90fcb83be..c693693f9 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationItem } from './locators/home'; import { open_Alice2 } from './state_builder'; @@ -24,14 +25,24 @@ bothPlatformsIt({ async function joinCommunityTest(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, alice2 }, - } = await open_Alice2({ platform, testInfo }); + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice2({ platform, testInfo }); + }); const testMessage = `Test message + ${new Date().getTime()}`; - - await joinCommunity(alice1, testCommunityLink, testCommunityName); - await sleepFor(5000); - await alice1.scrollToBottom(); - await alice1.sendMessage(testMessage); - // Has community synced to device 2? - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); - await closeApp(alice1, alice2); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + await sleepFor(5000); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { + await alice1.scrollToBottom(); + await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_SYNCED, async () => { + // Has community synced to device 2? + await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, alice2); + }); } diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index 47f0db209..ffaddd951 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -1,5 +1,6 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; import { MessageBody } from './locators/conversation'; @@ -24,10 +25,12 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te const { devices: { alice1, bob1 }, prebuilt: { alice, bob }, - } = await open_Alice1_Bob1_friends({ - platform, - testInfo, - focusFriendsConvo: true, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + testInfo, + focusFriendsConvo: true, + }); }); const testMessage = 'Checking disappear after read is working'; @@ -35,35 +38,46 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te // TODO: Consider refactoring DISAPPEARING_TIMES to include ms values const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; // Click conversation options menu (three dots) - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + await setDisappearingMessage( + platform, + alice1, + ['1:1', `Disappear after ${mode} option`, time], + bob1 + ); + }); // Check control message is correct on device 2 - await checkDisappearingControlMessage( - platform, - alice.userName, - bob.userName, - alice1, - bob1, - time, - mode - ); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + await checkDisappearingControlMessage( + platform, + alice.userName, + bob.userName, + alice1, + bob1, + time, + mode + ); + }); // Send message to verify that deletion is working - const sentTimestamp = await alice1.sendMessage(testMessage); - // NOTE we're only sending a text message, both devices are open, DaS is practically the same as DaR - await Promise.all( - [alice1, bob1].map(device => - device.hasElementDisappeared({ - ...new MessageBody(device, testMessage).build(), - maxWait, - actualStartTime: sentTimestamp, - }) - ) - ); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { + sentTimestamp = await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { + // NOTE we're only sending a text message, both devices are open, DaS is practically the same as DaR + await Promise.all( + [alice1, bob1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + }); // Great success - await closeApp(alice1, bob1); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); } diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index c163f8501..9cc57841e 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -45,7 +45,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { diff --git a/run/types/allure.ts b/run/types/allure.ts index adc7c7bb0..4703a6413 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -89,7 +89,7 @@ export const TestSteps = { }, // Disappearing Messages DISAPPEARING_MESSAGES: { - SET_DISAPPEARING_MSG: 'Set Disappearing Messages', + SET: (time: string) => `Set Disappearing Messages (${time})`, }, CALLS: { INITIATE_CALL: (userName: UserNameType) => `${userName} initiates voice call`, @@ -101,9 +101,11 @@ export const TestSteps = { `Verify ${elementDesc} element screenshot matches baseline`, GENERIC_MODAL: 'Verify modal strings', SPECIFIC_MODAL: (modalDesc: string) => `Verify ${modalDesc} modal strings`, + MESSAGE_SYNCED: 'Verify message synced to linked device', MESSAGE_RECEIVED: 'Verify message has been received', MESSAGE_DISAPPEARED: 'Verify message disappeared', MESSAGE_DELETED: (context: string) => `Verify message deleted in/on ${context}`, + DISAPPEARING_CONTROL_MESSAGES: 'Verify the disappearing control messages for each user', CALLING: 'Verify call has been started', CALL_SUCCESSFUL: 'Verify call has been put through successfully', MISSED_CALL: 'Verify missed call', From c9987d24a940e89dd315d920ee8ce3c0289c992a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 25 Sep 2025 14:20:34 +1000 Subject: [PATCH 71/88] fix: rename allure step --- .../specs/disappear_after_send_groups.spec.ts | 80 +++++++++++-------- run/test/specs/disappearing_link.spec.ts | 4 +- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 7399d8945..d46f24bea 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES } from '../../types/testing'; import { MessageBody } from './locators/conversation'; @@ -27,42 +28,53 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; const { devices: { alice1, bob1, charlie1 }, prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + }); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); + }); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + // Get correct control message for You setting disappearing messages + const disappearingMessagesSetYou = englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: controlMode }) + .toString(); + // Get correct control message for alice setting disappearing messages + const disappearingMessagesSetControl = englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: alice.userName, time, disappearing_messages_type: controlMode }) + .toString(); + // Check control message is correct on device 1, 2 and 3 + await Promise.all([ + alice1.waitForControlMessageToBePresent(disappearingMessagesSetYou), + bob1.waitForControlMessageToBePresent(disappearingMessagesSetControl), + charlie1.waitForControlMessageToBePresent(disappearingMessagesSetControl), + ]); }); - - await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); - // Get correct control message for You setting disappearing messages - const disappearingMessagesSetYou = englishStrippedStr('disappearingMessagesSetYou') - .withArgs({ time, disappearing_messages_type: controlMode }) - .toString(); - // Get correct control message for alice setting disappearing messages - const disappearingMessagesSetControl = englishStrippedStr('disappearingMessagesSet') - .withArgs({ name: alice.userName, time, disappearing_messages_type: controlMode }) - .toString(); - // Check control message is correct on device 1, 2 and 3 - await Promise.all([ - alice1.waitForControlMessageToBePresent(disappearingMessagesSetYou), - bob1.waitForControlMessageToBePresent(disappearingMessagesSetControl), - charlie1.waitForControlMessageToBePresent(disappearingMessagesSetControl), - ]); // Check for test messages (should be deleted) - const sentTimestamp = await alice1.sendMessage(testMessage); - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.hasElementDisappeared({ - ...new MessageBody(device, testMessage).build(), - maxWait, - actualStartTime: sentTimestamp, - }) - ) - ); - // Close server and devices - await closeApp(alice1, bob1, charlie1); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testGroupName), async () => { + sentTimestamp = await alice1.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.MESSAGE_DISAPPEARED, async () => { + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }) + ) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); } diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index ccd653ff1..0fdf1c374 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -49,7 +49,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); }); await test.step(TestSteps.SEND.LINK, async () => { @@ -103,7 +103,7 @@ async function disappearingLinkMessage1o1Android( testInfo, }); }); - await test.step(TestSteps.DISAPPEARING_MESSAGES.SET_DISAPPEARING_MSG, async () => { + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(platform, alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { From a48493b409a664bff1d1951c8ad926a35258fdd6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 25 Sep 2025 14:31:51 +1000 Subject: [PATCH 72/88] fix: no need to build the tests --- github/actions/setup/action.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index dc980cfe2..deca1e93e 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -26,9 +26,4 @@ runs: - name: Start Appium server shell: bash run: | - yarn "./node_modules/.bin/appium server --use-drivers=uiautomator2,xcuitest --port 8110 --allow-cors" - - - name: Build the tests files - shell: bash - run: | - yarn tsc + yarn start-server From 3a1e8ccffa0105abc18249f0f978ad0733b31a94 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 25 Sep 2025 16:49:03 +1000 Subject: [PATCH 73/88] fix: share extension icon uses uiselector query --- run/test/specs/locators/index.ts | 5 ++--- run/types/testing.ts | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index a6b84ab92..8a61169cc 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -460,9 +460,8 @@ export class ShareExtensionIcon extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'com.google.android.apps.photos:id/text', - text: `${getAppDisplayName()}`, // Session QA or AQA + strategy: '-android uiautomator', + selector: `new UiSelector().text("${getAppDisplayName()}")`, // Session QA or AQA }; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index e634fc656..9e15bca6d 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -423,7 +423,6 @@ export type Id = | 'android:id/alertTitle' | 'android:id/content_preview_text' | 'android:id/summary' - | 'android:id/text1' | 'android:id/title' | 'android.widget.TextView' | 'Appearance' @@ -444,7 +443,6 @@ export type Id = | 'com.android.permissioncontroller:id/permission_deny_button' | 'com.android.settings:id/switch_text' | 'com.google.android.apps.photos:id/sign_in_button' - | 'com.google.android.apps.photos:id/text' | 'Community input' | 'Confirm invite button' | 'Contact' From 092bacab82446ce801c5d1ce3f135f21c0d68d31 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 25 Sep 2025 17:12:53 +1000 Subject: [PATCH 74/88] feat: add more test.steps --- .../disappear_after_send_note_to_self.spec.ts | 66 +++++++++++-------- run/types/allure.ts | 1 + 2 files changed, 41 insertions(+), 26 deletions(-) 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 43d7e0bff..fb7ed8ad5 100644 --- a/run/test/specs/disappear_after_send_note_to_self.spec.ts +++ b/run/test/specs/disappear_after_send_note_to_self.spec.ts @@ -1,5 +1,6 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; import { MessageBody } from './locators/conversation'; @@ -24,36 +25,49 @@ bothPlatformsIt({ }); async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, testInfo: TestInfo) { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); const testMessage = `Testing disappearing messages in Note to Self`; - const alice = await newUser(device, USERNAME.ALICE); const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer + let sentTimestamp: number; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); // Send message to self to bring up Note to Self conversation - await device.clickOnElementAll(new PlusButton(device)); - await device.clickOnElementAll(new NewMessageOption(device)); - await device.inputText(alice.accountID, new EnterAccountID(device)); - await device.scrollDown(); - await device.clickOnElementAll(new NextButton(device)); - await device.sendMessage('Buy milk'); - // Enable disappearing messages - await setDisappearingMessage(platform, device, [ - 'Note to Self', - 'Disappear after send option', - time, - ]); - await sleepFor(1000); - await device.waitForControlMessageToBePresent( - `You set messages to disappear ${time} after they have been ${controlMode}.` - ); - const sentTimestamp = await device.sendMessage(testMessage); - await device.hasElementDisappeared({ - ...new MessageBody(device, testMessage).build(), - maxWait, - actualStartTime: sentTimestamp, + await test.step(TestSteps.OPEN.NTS, async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(alice.accountID, new EnterAccountID(device)); + await device.scrollDown(); + await device.clickOnElementAll(new NextButton(device)); + }); + await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { + // Enable disappearing messages + await setDisappearingMessage(platform, device, [ + 'Note to Self', + 'Disappear after send option', + time, + ]); + await sleepFor(1000); + await device.waitForControlMessageToBePresent( + `You set messages to disappear ${time} after they have been ${controlMode}.` + ); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, 'Note to Self'), async () => { + sentTimestamp = await device.sendMessage(testMessage); + }); + await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { + await device.hasElementDisappeared({ + ...new MessageBody(device, testMessage).build(), + maxWait, + actualStartTime: sentTimestamp, + }); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + // Great success + await closeApp(device); }); - // Great success - await closeApp(device); } diff --git a/run/types/allure.ts b/run/types/allure.ts index 4703a6413..7ee2167be 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -77,6 +77,7 @@ export const TestSteps = { }, // Open/Navigate steps OPEN: { + NTS: 'Open Note to Self', UPDATE_GROUP_INFO: `Open 'Update Group Information' modal`, PATH: 'Open Path screen', APPEARANCE: 'Open Appearance settings', From 6b1b64838c50dc10b0eede5c67e6bf8797f3066e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 29 Sep 2025 10:06:34 +1000 Subject: [PATCH 75/88] Revert "fix: no need to build the tests" This reverts commit a48493b409a664bff1d1951c8ad926a35258fdd6. --- github/actions/setup/action.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index deca1e93e..dc980cfe2 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -26,4 +26,9 @@ runs: - name: Start Appium server shell: bash run: | - yarn start-server + yarn "./node_modules/.bin/appium server --use-drivers=uiautomator2,xcuitest --port 8110 --allow-cors" + + - name: Build the tests files + shell: bash + run: | + yarn tsc From f1785b64a65703c3f144be8b7a4a08a3708e5864 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 29 Sep 2025 15:27:07 +1000 Subject: [PATCH 76/88] chore: tweak long message test --- .../specs/group_message_long_text.spec.ts | 1 - run/test/specs/locators/conversation.ts | 18 +++-- run/test/specs/message_long_text.spec.ts | 34 +++++++--- run/types/DeviceWrapper.ts | 65 +++++++------------ run/types/testing.ts | 1 + 5 files changed, 66 insertions(+), 53 deletions(-) diff --git a/run/test/specs/group_message_long_text.spec.ts b/run/test/specs/group_message_long_text.spec.ts index 70da9c211..9122cb964 100644 --- a/run/test/specs/group_message_long_text.spec.ts +++ b/run/test/specs/group_message_long_text.spec.ts @@ -34,7 +34,6 @@ async function sendLongMessageGroup(platform: SupportedPlatformsType, testInfo: const replyMessage = await bob1.replyToMessage(alice, longText); await Promise.all( [alice1, charlie1].map(async device => { - await device.scrollToBottom(); await device.waitForTextElementToBePresent(new MessageBody(device, replyMessage)); }) ); diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 2d81e2423..960237075 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -174,12 +174,22 @@ export class AttachmentsButton extends LocatorsInterface { } } +// TODO tie this to the message whose status we want to check (similar to EmojiReactsPill) export class OutgoingMessageStatusSent extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: `Message sent status: Sent`, - } as const; + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("network.loki.messenger.qa:id/messageStatusTextView").text("Sent")', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: `Message sent status: Sent`, + } as const; + } } } diff --git a/run/test/specs/message_long_text.spec.ts b/run/test/specs/message_long_text.spec.ts index ad4312aff..a4290022d 100644 --- a/run/test/specs/message_long_text.spec.ts +++ b/run/test/specs/message_long_text.spec.ts @@ -2,7 +2,12 @@ import type { TestInfo } from '@playwright/test'; import { longText } from '../../constants'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { MessageBody } from './locators/conversation'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; @@ -45,11 +50,9 @@ async function sendLongMessageIos(platform: SupportedPlatformsType, testInfo: Te } async function sendLongMessageAndroid(platform: SupportedPlatformsType, testInfo: TestInfo) { - // Sending a long text message - // Open device and server const { devices: { alice1, bob1 }, - prebuilt: { alice }, + prebuilt: { bob }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, @@ -57,10 +60,25 @@ async function sendLongMessageAndroid(platform: SupportedPlatformsType, testInfo }); // Send a long message from User A to User B await alice1.sendMessage(longText); - // Reply to message (User B to User A) - const sentMessage = await bob1.replyToMessage(alice, longText); - // Check reply came through on alice1 - await alice1.waitForTextElementToBePresent(new MessageBody(alice1, sentMessage)); + // Bob replies + await bob1.longPressMessage(longText); + await bob1.clickOnByAccessibilityID('Reply to message'); + + const replyMessage = `${bob.userName} replied to ${longText}`; + await bob1.inputText(replyMessage, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + + // This is dumb. The CI doesn't scroll to bottom when ran through Github Actions. + // If you start the emulators on the CI box yourself the test will pass. I have no idea why. + if (process.env.GITHUB_ACTIONS) { + await bob1.scrollToBottom(); + } + + await bob1.waitForTextElementToBePresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 50000, + }); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, replyMessage)); // Close app await closeApp(alice1, bob1); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 499ec2062..10fb89bbf 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -693,23 +693,25 @@ export class DeviceWrapper { } public async longPressMessage(textToLookFor: string) { - const maxRetries = 3; - let attempt = 0; - let success = false; + const truncatedText = + textToLookFor.length > 50 ? textToLookFor.substring(0, 50) + '...' : textToLookFor; - while (attempt < maxRetries && !success) { - try { + const result = await this.pollUntil( + async () => { + // Find the message const el = await this.waitForTextElementToBePresent({ ...new MessageBody(this, textToLookFor).build(), maxWait: 1_000, }); + if (!el) { - throw new Error( - `longPress on message: ${textToLookFor} unsuccessful, couldn't find message` - ); + return { success: false, error: `Couldn't find message: ${truncatedText}` }; } - await this.longClick(el, 4000); + // Attempt long click + await this.longClick(el, 2000); + + // Check if context menu appeared const longPressSuccess = await this.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Reply to message', @@ -718,21 +720,22 @@ export class DeviceWrapper { if (longPressSuccess) { this.log('LongClick successful'); - success = true; // Exit the loop if successful - } else { - throw new Error(`longPress on message: ${textToLookFor} unsuccessful`); - } - } catch (error) { - attempt++; - if (attempt >= maxRetries) { - throw new Error( - `Longpress on message: ${textToLookFor} unsuccessful after ${maxRetries} attempts, ${(error as Error).toString()}` - ); + return { success: true, data: el }; } - this.log(`Longpress attempt ${attempt} failed. Retrying...`); - await sleepFor(1000); + + return { + success: false, + error: `Long press didn't show context menu for: ${truncatedText}`, + }; + }, + { + maxWait: 10_000, + pollInterval: 1000, + onAttempt: attempt => this.log(`Longpress attempt ${attempt}...`), } - } + ); + + return result; // or whatever you want to do with it } public async longPressConversation(userName: string) { @@ -1697,24 +1700,6 @@ export class DeviceWrapper { return sentTimestamp; } - public async waitForSentConfirmation() { - let pendingStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Sending', - }); - const failedStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Failed to send', - }); - if (pendingStatus || failedStatus) { - await sleepFor(100); - pendingStatus = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Message sent status: Sending', - }); - } - } - public async sendNewMessage(user: Pick, message: string) { // Sender workflow // Click on plus button diff --git a/run/types/testing.ts b/run/types/testing.ts index 9e15bca6d..78233f623 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -166,6 +166,7 @@ export type UiAutomatorQuery = | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' + | 'new UiSelector().resourceId("network.loki.messenger.qa:id/messageStatusTextView").text("Sent")' | 'new UiSelector().text("Enter your display name")' | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` | `new UiSelector().text(${string})`; From d37294b34184af7ea79c48cc0f8970228889c3d2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 15 Sep 2025 10:49:19 +1000 Subject: [PATCH 77/88] feat: simplify logging fix: yml syntax --- .github/workflows/android-regression.yml | 40 ++++++++---------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 66f9d15de..3523cffba 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -43,30 +43,15 @@ on: - '2' default: '0' - PRINT_FAILED_TEST_LOGS: - description: 'print failed test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' + LOG_LEVEL: + description: 'Test logging verbosity (WARNING: anything other than minimal mode may crash GitHub Actions on large test runs)' required: true type: choice options: - - '0' - - '1' - default: '0' - PRINT_ONGOING_TEST_LOGS: - description: 'print ongoing test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' - required: true - type: choice - options: - - '0' - - '1' - default: '0' - HIDE_WEBDRIVER_LOGS: - description: 'print webdriver logs (1 to hide, 0 to show). PRINT_ONGOING_TEST_LOGS or PRINT_FAILED_TEST_LOGS must be 1' - required: true - type: choice - options: - - '0' - - '1' - default: '1' + - 'minimal' # Recommended for full regressions + - 'failures' # Show failed test logs only + - 'verbose' # All test logs - use with caution! + default: 'minimal' jobs: android-regression: @@ -83,8 +68,9 @@ jobs: APPIUM_ADB_FULL_PATH: '/opt/android/platform-tools/adb' ANDROID_SDK_ROOT: '/opt/android' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.PRINT_FAILED_TEST_LOGS }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.PRINT_ONGOING_TEST_LOGS }} + PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} + PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} + HIDE_WEBDRIVER_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'minimal' && '1' || '0' }} IOS_1_SIMULATOR: '' IOS_2_SIMULATOR: '' IOS_3_SIMULATOR: '' @@ -107,15 +93,15 @@ jobs: uses: ./github/actions/fetch-allure-history if: ${{ env.ALLURE_ENABLED == 'true' }} with: - PLATFORM: ${{env.PLATFORM}} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN}} + PLATFORM: ${{ env.PLATFORM }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: ./github/actions/print-runner-details with: APK_URL: ${{ github.event.inputs.APK_URL }} RISK: ${{ github.event.inputs.RISK }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.PRINT_FAILED_TEST_LOGS }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.PRINT_ONGOING_TEST_LOGS }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - name: Download APK & extract it From bf8b5389ea47985e85071e788c88391ea469f007 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 30 Sep 2025 14:30:03 +1000 Subject: [PATCH 78/88] fix: remove obsolete pagescreenshot methods --- .github/workflows/android-regression.yml | 6 +-- run/test/specs/utils/screenshot_paths.ts | 8 ---- run/test/specs/utils/verify_screenshots.ts | 52 ++-------------------- run/types/allure.ts | 2 +- run/types/testing.ts | 2 - 5 files changed, 7 insertions(+), 63 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 3523cffba..5314b0859 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -48,9 +48,9 @@ on: required: true type: choice options: - - 'minimal' # Recommended for full regressions - - 'failures' # Show failed test logs only - - 'verbose' # All test logs - use with caution! + - 'minimal' # Recommended for full regressions + - 'failures' # Show failed test logs only + - 'verbose' # All test logs - use with caution! default: 'minimal' jobs: diff --git a/run/test/specs/utils/screenshot_paths.ts b/run/test/specs/utils/screenshot_paths.ts index f03aaed68..1bff8b8e5 100644 --- a/run/test/specs/utils/screenshot_paths.ts +++ b/run/test/specs/utils/screenshot_paths.ts @@ -1,9 +1,7 @@ import path from 'path'; -import { PageName } from '../../../types/testing'; import { EmptyLandingPage } from '../locators/home'; import { AppDisguisePage } from '../locators/settings'; -import { SupportedPlatformsType } from './open_app'; // Extends locator classes with baseline screenshot paths for visual regression testing // If a locator appears in multiple states, a state argument must be provided to screenshotFileName() @@ -15,12 +13,6 @@ export class EmptyLandingPageScreenshot extends EmptyLandingPage { } } -export class BrowserPageScreenshot { - public screenshotFileName(platform: SupportedPlatformsType, pageName: PageName): string { - return path.join('run', 'screenshots', platform, `browser_${pageName}.png`); - } -} - export class AppDisguisePageScreenshot extends AppDisguisePage { public screenshotFileName(): string { return path.join('run', 'screenshots', this.platform, 'app_disguise.png'); diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 9b3cb3e9b..f949acce4 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -5,11 +5,8 @@ import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; -import { PageName } from '../../../types/testing'; import { LocatorsInterfaceScreenshot } from '../locators'; -import { SupportedPlatformsType } from './open_app'; -import { BrowserPageScreenshot } from './screenshot_paths'; -import { cropScreenshot, getDiffDirectory, saveImage } from './utilities'; +import { getDiffDirectory } from './utilities'; type Attachment = { name: string; @@ -114,7 +111,8 @@ export async function verifyElementScreenshot< contentType: 'image/png', }, ]); - throw new Error(`The images do not match. The diff has been saved to ${diffImagePath}`); + console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`); + throw new Error(`The UI doesn't match expected appearance`); } // Cleanup of element screenshot file on success @@ -128,47 +126,3 @@ export async function verifyElementScreenshot< } } } - -export async function verifyPageScreenshot( - platform: SupportedPlatformsType, - device: DeviceWrapper, - page: PageName -): Promise { - // Create file path for the diff image (if doesn't exist) - const diffsDir = getDiffDirectory(); - // Capture screenshot - const screenshotBase64 = await device.getScreenshot(); - const screenshotBuffer = Buffer.from(screenshotBase64, 'base64'); - // Need to crop screenshot to cut out time - const croppedBuf = await cropScreenshot(device, screenshotBuffer); - // Create file path for the screenshot - const screenshotName = await saveImage(croppedBuf, diffsDir, 'screenshot'); - // Create custom file path for the baseline screenshot - const baselinePath = new BrowserPageScreenshot().screenshotFileName(platform, page); - fs.mkdirSync(path.dirname(baselinePath), { recursive: true }); - - if (!fs.existsSync(baselinePath)) { - fs.writeFileSync(baselinePath, croppedBuf); - console.warn(`No baseline existed – created new baseline for "${page}" at:\n ${baselinePath}`); - return; - } - // otherwise compare against the existing baseline - const { equal, diffImage } = await looksSame(croppedBuf, baselinePath, { - createDiffImage: true, - }); - - if (!equal) { - const diffImagePath = await saveImage(diffImage, diffsDir, 'diff'); - console.log(`Visual comparison failed. The diff has been saved to ${diffImagePath}`); - throw new Error(`The UI doesn't match expected appearance`); - } - // Cleanup of element screenshot file on success - try { - fs.unlinkSync(screenshotName); - console.log('Temporary screenshot deleted successfully'); - } catch (err) { - if (err instanceof Error) { - console.error(`Error deleting file: ${err.message}`); - } - } -} diff --git a/run/types/allure.ts b/run/types/allure.ts index 7ee2167be..606b5f40b 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -77,7 +77,7 @@ export const TestSteps = { }, // Open/Navigate steps OPEN: { - NTS: 'Open Note to Self', + NTS: 'Open Note to Self', UPDATE_GROUP_INFO: `Open 'Update Group Information' modal`, PATH: 'Open Path screen', APPEARANCE: 'Open Appearance settings', diff --git a/run/types/testing.ts b/run/types/testing.ts index 78233f623..68dfa8aad 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -576,8 +576,6 @@ export type TestRisk = 'high' | 'low' | 'medium'; export type ElementStates = 'new_account' | 'restore_account'; -export type PageName = 'network_page' | 'staking_page'; - export type Suffix = 'diff' | 'screenshot'; export type AppName = 'Session AQA' | 'Session QA'; From 9794f735a7669e1ae77168ec6b9e8f3a2bc3d0e8 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 30 Sep 2025 15:29:04 +1000 Subject: [PATCH 79/88] chore: update ios 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 926271317..7950444a3 100644 --- a/run/screenshots/ios/app_disguise.png +++ b/run/screenshots/ios/app_disguise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74c3f2a2e564e6e78c245d2fd5cf410e930c76672a839dedf85e5177a79ed5ea -size 476984 +oid sha256:f215acd1deb44135705057cc83a0f3997ce999c456783fa3c1209a33b1ca3c48 +size 477016 From f6f87645230b41e5e4722ab5a0a5d0540b138b1a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 30 Sep 2025 16:02:53 +1000 Subject: [PATCH 80/88] fix: blind sleep in trustAttachments on iOS --- run/types/DeviceWrapper.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 10fb89bbf..62dfb10e4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2221,6 +2221,12 @@ export class DeviceWrapper { } public async trustAttachments(conversationName: string) { + // I kept getting stale element references on iOS in this method + // This is an attempt to let the UI settle before we look for the untrusted attachment + if (this.isIOS()) { + await sleepFor(2000); + } + await this.clickOnElementAll({ strategy: 'accessibility id', selector: 'Untrusted attachment message', From c0dd657832866ac4e1dbca40e64eb43199753358 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 30 Sep 2025 16:23:54 +1000 Subject: [PATCH 81/88] chore: tidy up matchAndTapImage logging --- run/types/DeviceWrapper.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 62dfb10e4..869eab614 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1017,7 +1017,7 @@ export class DeviceWrapper { // Find all candidate elements matching the locator const elements = await this.findElements(locator.strategy, locator.selector); this.info( - `[matchAndTapImage] Found ${elements.length} elements for ${locator.strategy} "${locator.selector}"` + `[matchAndTapImage] Starting image matching: ${elements.length} elements with ${locator.strategy} "${locator.selector}"` ); // Load the reference image buffer from disk @@ -1030,8 +1030,7 @@ export class DeviceWrapper { } | null = null; // Iterate over each candidate element - for (const [i, el] of elements.entries()) { - this.info(`[matchAndTapImage] Processing element ${i + 1}/${elements.length}`); + for (const el of elements) { // Take a screenshot of the element const base64 = await this.getElementScreenshot(el.ELEMENT); @@ -1064,7 +1063,6 @@ export class DeviceWrapper { const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { threshold, }); - this.info(`[matchAndTapImage] Match score for element ${i + 1}: ${score.toFixed(4)}`); /** * Matching is done on a resized reference image to account for device pixel density. @@ -1097,7 +1095,7 @@ export class DeviceWrapper { // If earlyMatch is enabled and the score is high enough, tap immediately if (earlyMatch && score >= earlyMatchThreshold) { this.info( - `[matchAndTapImage] Tapping first match with ${(score * 100).toFixed(2)}% confidence` + `[matchAndTapImage] Tapping first high-confidence match (${(score * 100).toFixed(2)}%)` ); await clickOnCoordinates(this, center); return; @@ -1105,21 +1103,17 @@ export class DeviceWrapper { // Otherwise, keep track of the best match so far if (!bestMatch || score > bestMatch.score) { bestMatch = { center, score }; - this.info(`[matchAndTapImage] New best match: ${(score * 100).toFixed(2)}% confidence`); } - } catch (err) { - // If matching fails for this element, log and continue to the next - this.info( - `[matchAndTapImage] Matching failed for element ${i + 1}:`, - err instanceof Error ? err.message : err - ); + } catch { + continue; // No match in this element, try next } } // If no good match was found, throw an error if (!bestMatch) { - throw new Error( + console.log( `[matchAndTapImage] No matching image found among ${elements.length} elements for ${locator.strategy} "${locator.selector}"` ); + throw new Error('Unable to find the expected UI element on screen'); } // Tap the best match found this.info( From 093e2c6bc58e707558ced526241ed79c74bbbd1d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 1 Oct 2025 14:38:54 +1000 Subject: [PATCH 82/88] fix: simplify CI logging --- .github/workflows/android-regression.yml | 29 ++++++++++------ .github/workflows/ios-regression.yml | 42 +++++++++--------------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 5314b0859..b19dbcb3e 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -49,8 +49,8 @@ on: type: choice options: - 'minimal' # Recommended for full regressions - - 'failures' # Show failed test logs only - - 'verbose' # All test logs - use with caution! + - 'failures' # Show failed test logs stdout/stderr + - 'verbose' # Ongoing and failed test logs default: 'minimal' jobs: @@ -68,9 +68,9 @@ jobs: APPIUM_ADB_FULL_PATH: '/opt/android/platform-tools/adb' ANDROID_SDK_ROOT: '/opt/android' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} - HIDE_WEBDRIVER_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'minimal' && '1' || '0' }} + _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) + PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) + PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) IOS_1_SIMULATOR: '' IOS_2_SIMULATOR: '' IOS_3_SIMULATOR: '' @@ -152,9 +152,12 @@ jobs: env: PLAYWRIGHT_WORKERS_COUNT: 4 DEVICES_PER_TEST_COUNT: 1 + _TESTING: ${{ env._TESTING }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - TESTING=${{ github.event.inputs.HIDE_WEBDRIVER_LOGS }} npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes - name: Upload results of this run uses: ./github/actions/upload-test-results @@ -168,9 +171,12 @@ jobs: env: PLAYWRIGHT_WORKERS_COUNT: 2 DEVICES_PER_TEST_COUNT: 2 + _TESTING: ${{ env._TESTING }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - _TESTING=${{ github.event.inputs.HIDE_WEBDRIVER_LOGS }} npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes - name: Upload results of this run uses: ./github/actions/upload-test-results @@ -178,15 +184,18 @@ jobs: PLATFORM: ${{ env.PLATFORM }} UPLOAD_IDENTIFIER: 'devices-2-test-run' - - name: Run the tests ​​other tests with 1 worker + - name: Run the other tests with 1 worker continue-on-error: true id: other-devices-test-run env: PLAYWRIGHT_WORKERS_COUNT: 1 DEVICES_PER_TEST_COUNT: 4 + _TESTING: ${{ env._TESTING }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - _TESTING=${{ github.event.inputs.HIDE_WEBDRIVER_LOGS }} npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" --grep-invert "@1-devices|@2-devices" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" --grep-invert "@1-devices|@2-devices" #Note: this has to be double quotes - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report @@ -213,7 +222,7 @@ jobs: PLATFORM: ${{ env.PLATFORM }} - name: Check if any tests failed - if: steps.devices-1-test-run != 'success' || steps.devices-2-test-run != 'success' || steps.other-devices-test-run.outcome != 'success' + if: steps.devices-1-test-run.outcome != 'success' || steps.devices-2-test-run.outcome != 'success' || steps.other-devices-test-run.outcome != 'success' run: echo 'Some test failed, see above'; exit 1 - name: Stop emulators diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 85dc2559d..65002a275 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -43,22 +43,6 @@ on: - '2' default: '0' - PRINT_FAILED_TEST_LOGS: - description: 'print failed test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' - required: true - type: choice - options: - - '0' - - '1' - default: '0' - PRINT_ONGOING_TEST_LOGS: - description: 'print ongoing test logs (1 to enable) - DONT DO FOR FULL REGRESSION (it crashes github)' - required: true - type: choice - options: - - '0' - - '1' - default: '0' PLAYWRIGHT_WORKERS_COUNT: description: 'number of workers to use (ios only)' required: true @@ -68,14 +52,16 @@ on: - '2' - '3' default: '3' - HIDE_WEBDRIVER_LOGS: - description: 'print webdriver logs (1 to hide, 0 to show). PRINT_ONGOING_TEST_LOGS or PRINT_FAILED_TEST_LOGS must be 1' + + LOG_LEVEL: + description: 'Test logging verbosity (WARNING: anything other than minimal mode may crash GitHub Actions on large test runs)' required: true type: choice options: - - '0' - - '1' - default: '1' + - 'minimal' # Recommended for full regressions + - 'failures' # Show failed test logs only + - 'verbose' # All test logs - use with caution! + default: 'minimal' jobs: ios-regression: @@ -92,8 +78,9 @@ jobs: APPIUM_ADB_FULL_PATH: '' ANDROID_SDK_ROOT: '' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} - PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.PRINT_FAILED_TEST_LOGS }} - PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.PRINT_ONGOING_TEST_LOGS }} + _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) + PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) + PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) PLAYWRIGHT_WORKERS_COUNT: 3 # for iOS, this is the max we can have on our self-hosted runner SDK_MANAGER_FULL_PATH: '' AVD_MANAGER_FULL_PATH: '' @@ -121,8 +108,8 @@ jobs: uses: ./github/actions/fetch-allure-history if: ${{ env.ALLURE_ENABLED == 'true' }} with: - PLATFORM: ${{env.PLATFORM}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + PLATFORM: ${{ env.PLATFORM }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download ipa and extract it run: | @@ -158,9 +145,12 @@ jobs: - name: Run the iOS tests​​ (all device counts) env: DEVICES_PER_TEST_COUNT: 4 + _TESTING: ${{ env._TESTING }} + PRINT_FAILED_TEST_LOGS: ${{ env.PRINT_FAILED_TEST_LOGS }} + PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - _TESTING=${{ github.event.inputs.HIDE_WEBDRIVER_LOGS }} npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report From d1410452ec1b77baab4debc112f356d4084c7fc2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 1 Oct 2025 14:59:16 +1000 Subject: [PATCH 83/88] chore: tidy up PR chore: remove debug line --- run/test/specs/disappearing_gif.spec.ts | 2 +- .../group_disappearing_messages_gif.spec.ts | 3 +-- .../group_disappearing_messages_image.spec.ts | 20 ++++++++----------- .../linked_device_change_username.spec.ts | 6 +++++- .../specs/linked_device_restore_group.spec.ts | 10 +++++----- .../specs/user_actions_set_nickname.spec.ts | 3 --- run/types/DeviceWrapper.ts | 8 ++------ 7 files changed, 22 insertions(+), 30 deletions(-) diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 657f08802..220924bda 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -22,7 +22,7 @@ bothPlatformsIt({ // The timing with 30 seconds was a bit tight in terms of the attachment downloading and becoming visible const time = DISAPPEARING_TIMES.ONE_MINUTE; const initialMaxWait = 15_000; // GIFs could be large so give them a bit more time to be found -const maxWait = 70_000; // 70s plus buffer +const maxWait = 70_000; // 60s plus buffer const timerType = 'Disappear after send option'; async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index c5af419ea..7d396e0fe 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -23,7 +23,7 @@ bothPlatformsIt({ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; const initialMaxWait = 15_000; // Downloading the attachment can take a while -const maxWait = 70_000; // 70s plus buffer +const maxWait = 70_000; // 60s plus buffer async function disappearingGifMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Disappear after sent test'; @@ -38,7 +38,6 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); // Click on attachments button const sentTimestamp = await alice1.sendGIF(); - console.log(`the sent timestamp is ${sentTimestamp}`); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) ); diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 5dea27a3f..c8fe12786 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -48,22 +48,18 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t ); } if (platform === 'android') { - await Promise.all([ - alice1.hasElementDisappeared({ - ...new MediaMessage(alice1).build(), - maxWait, - actualStartTime: sentTimestamp, - }), - // Bob and Charlie haven't trusted the message - ...[bob1, charlie1].map(device => + await Promise.all( + [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) + ); + await Promise.all( + [alice1, bob1, charlie1].map(device => device.hasElementDisappeared({ - strategy: 'accessibility id', - selector: 'Untrusted attachment message', + ...new MediaMessage(device).build(), maxWait, actualStartTime: sentTimestamp, }) - ), - ]); + ) + ); } await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/linked_device_change_username.spec.ts b/run/test/specs/linked_device_change_username.spec.ts index 49e9312e5..38e93bb34 100644 --- a/run/test/specs/linked_device_change_username.spec.ts +++ b/run/test/specs/linked_device_change_username.spec.ts @@ -33,6 +33,10 @@ async function changeUsernameLinked(platform: SupportedPlatformsType, testInfo: await alice1.onAndroid().clickOnElementAll(new ClearInputButton(alice1)); await alice1.inputText(newUsername, new UsernameInput(alice1)); await alice1.clickOnElementAll(new SaveNameChangeButton(alice1)); - await alice2.waitForTextElementToBePresent(new UsernameDisplay(alice2, newUsername)); + await Promise.all( + [alice1, alice2].map(device => + device.waitForTextElementToBePresent(new UsernameDisplay(device, newUsername)) + ) + ); await closeApp(alice1, alice2); } diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 2c77a9faf..e90c6e485 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -34,11 +34,11 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo // Check the group name has loaded await device4.waitForTextElementToBePresent(new ConversationHeaderName(device4, testGroupName)); // Check all messages are present - await Promise.all([ - device4.waitForTextElementToBePresent(new MessageBody(device4, aliceMessage)), - device4.waitForTextElementToBePresent(new MessageBody(device4, bobMessage)), - device4.waitForTextElementToBePresent(new MessageBody(device4, charlieMessage)), - ]); + await Promise.all( + [aliceMessage, bobMessage, charlieMessage].map(message => + device4.waitForTextElementToBePresent(new MessageBody(device4, message)) + ) + ); const testMessage2 = 'Checking that message input is working'; await device4.sendMessage(testMessage2); await Promise.all( diff --git a/run/test/specs/user_actions_set_nickname.spec.ts b/run/test/specs/user_actions_set_nickname.spec.ts index 5ba9d51d6..253ee7a4c 100644 --- a/run/test/specs/user_actions_set_nickname.spec.ts +++ b/run/test/specs/user_actions_set_nickname.spec.ts @@ -26,9 +26,6 @@ bothPlatformsIt({ suite: 'Set Nickname', }, allureDescription: `Verifies that a user can set a nickname for a contact and that it appears correctly in the conversation settings, conversation header and home screen.`, - allureLinks: { - android: 'SES-4424', - }, }); async function setNickname(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 869eab614..53f9d118c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -310,7 +310,7 @@ export class DeviceWrapper { // ELEMENT INTERACTION - // Heal a broken locator by finding potential fuzzy matches with text as first-class criteria + // Heal a broken locator by finding potential fuzzy matches with text private async findBestMatch( strategy: Strategy, selector: string, @@ -389,7 +389,7 @@ export class DeviceWrapper { continue; } - // Sometimes the element is just not on screen yet - skip + // Sometimes the element is just not on screen yet - proceed. if (match.strategy === strategy && match.originalSelector === selector) { continue; } @@ -1031,7 +1031,6 @@ export class DeviceWrapper { // Iterate over each candidate element for (const el of elements) { - // Take a screenshot of the element const base64 = await this.getElementScreenshot(el.ELEMENT); const elementBuffer = Buffer.from(base64, 'base64'); @@ -1683,8 +1682,6 @@ export class DeviceWrapper { if (!sendButton) { throw new Error('Send button not found: Need to restart iOS emulator: Known issue'); } - // Might need to scroll down if the message is too long - // await this.scrollToBottom(); TODO temporarily disabling this to verify // Wait for tick await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), @@ -1986,7 +1983,6 @@ export class DeviceWrapper { maxWait: 20000, }); const sentTimestamp = Date.now(); - this.log(`[DEBUG] sendVideoiOS returning timestamp: ${sentTimestamp}`); return sentTimestamp; } From 1cf2d166d2928462307e23bbc7589bb3deeb0bb1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 10:45:59 +1100 Subject: [PATCH 84/88] fix: use sent status locator --- run/test/specs/message_requests_accept_text_reply.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 fa62e383e..8337631b8 100644 --- a/run/test/specs/message_requests_accept_text_reply.spec.ts +++ b/run/test/specs/message_requests_accept_text_reply.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { MessageInput, SendButton } from './locators/conversation'; +import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; import { PlusButton } from './locators/home'; import { MessageRequestsBanner } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; @@ -54,11 +54,7 @@ async function acceptRequestWithText(platform: SupportedPlatformsType, testInfo: // Click send await device1.clickOnElementAll(new SendButton(device1)); // Wait for tick - await device1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: `Message sent status: Sent`, - maxWait: 50000, - }); + await device1.waitForTextElementToBePresent(new OutgoingMessageStatusSent(device1)); // Wait for banner to appear // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); From ae293af13d9dbd7438152423ca947ea8025e938a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 11:27:43 +1100 Subject: [PATCH 85/88] fix: SES-4192 changes --- run/test/specs/voice_calls.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 4241abd73..66f608e3c 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -82,8 +82,11 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo // Alice tries again, call is created but Bob still hasn't enabled their calls perms so this will fail await test.step(TestSteps.CALLS.INITIATE_CALL(alice.userName), async () => { await alice1.clickOnElementAll(new CallButton(alice1)); - // The Missed call modal is currently not exposed so the test just dismisses with a button press, see SES-4192 - await bob1.clickOnElementXPath(`//XCUIElementTypeButton[@name="Settings"]`); + await bob1.checkModalStrings( + englishStrippedStr('callsMissedCallFrom').withArgs({ name: alice.userName }).toString(), + englishStrippedStr('callsYouMissedCallPermissions').withArgs({ name: alice.userName }).toString() + ); + await bob1.clickOnByAccessibilityID('Settings'); await alice1.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Ringing...', From bf588ec9b115df285e84e2460098ec420f1c3519 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 14:46:05 +1100 Subject: [PATCH 86/88] fix: reduce uiscrollable complexity --- run/test/specs/locators/settings.ts | 10 +++++----- run/types/testing.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index ec4c556e8..b485be8aa 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -166,7 +166,7 @@ export class ConversationsMenuItem extends LocatorsInterface { return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Conversations"))', } as const; case 'ios': return { @@ -184,7 +184,7 @@ export class AppearanceMenuItem extends LocatorsInterface { return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Appearance"))', } as const; case 'ios': return { @@ -219,7 +219,7 @@ export class SelectAppIcon extends LocatorsInterface { return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Select app icon"))', } as const; case 'ios': return { @@ -300,7 +300,7 @@ export class PathMenuItem extends LocatorsInterface { return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', } as const; case 'ios': return { @@ -318,7 +318,7 @@ export class VersionNumber extends LocatorsInterface { return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith("Version"))', } as const; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index 68dfa8aad..0938cb918 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -161,11 +161,11 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; export type UiAutomatorQuery = - | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Appearance"))' - | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("Conversations"))' - | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' - | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().text("Select app icon"))' - | 'new UiScrollable(new UiSelector().className("android.widget.ScrollView")).scrollIntoView(new UiSelector().textStartsWith("Version"))' + | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Appearance"))' + | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Conversations"))' + | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' + | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Select app icon"))' + | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().resourceId("network.loki.messenger.qa:id/messageStatusTextView").text("Sent")' | 'new UiSelector().text("Enter your display name")' | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` From 94a229a3bb38f2ca4dec890917267a882df3a735 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 14:46:32 +1100 Subject: [PATCH 87/88] fix: log when skipping healing --- run/types/DeviceWrapper.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 53f9d118c..ff21dfcb0 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -329,6 +329,8 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; + + // If this list gets out of hand, consider lowering the threshold const blacklist = [ { from: 'Voice message', to: 'New voice message' }, { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, @@ -386,6 +388,7 @@ export class DeviceWrapper { // Don't heal blacklisted pairs if (isBlacklisted) { + this.log(`Skipping healing: prevented "${selector}" from healing to "${match.originalSelector}"`); continue; } From 21ecab7e6fcb4b73f03661d4a6926c2358c1aa4d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 6 Oct 2025 14:46:56 +1100 Subject: [PATCH 88/88] chore: linting --- run/test/specs/voice_calls.spec.ts | 4 +++- run/types/DeviceWrapper.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 66f608e3c..0df4e28b6 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -84,7 +84,9 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo await alice1.clickOnElementAll(new CallButton(alice1)); await bob1.checkModalStrings( englishStrippedStr('callsMissedCallFrom').withArgs({ name: alice.userName }).toString(), - englishStrippedStr('callsYouMissedCallPermissions').withArgs({ name: alice.userName }).toString() + englishStrippedStr('callsYouMissedCallPermissions') + .withArgs({ name: alice.userName }) + .toString() ); await bob1.clickOnByAccessibilityID('Settings'); await alice1.waitForTextElementToBePresent({ diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index ff21dfcb0..af1ededcf 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -329,7 +329,6 @@ export class DeviceWrapper { { strategy: 'id' as Strategy, pattern: /resource-id="([^"]+)"/g }, ]; - // If this list gets out of hand, consider lowering the threshold const blacklist = [ { from: 'Voice message', to: 'New voice message' }, @@ -388,7 +387,9 @@ export class DeviceWrapper { // Don't heal blacklisted pairs if (isBlacklisted) { - this.log(`Skipping healing: prevented "${selector}" from healing to "${match.originalSelector}"`); + this.log( + `Skipping healing: prevented "${selector}" from healing to "${match.originalSelector}"` + ); continue; }