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

{account_id}

Download it at https://getsession.org/download", - args: { account_id: 'string' }, + en: "Hey, I've been using Session to chat with complete privacy and security. Come join me! My Account ID is

{account_id}

Download it at https://getsession.org/download", + args: {account_id: "string"} }, accountIdYours: { - en: 'Your Account ID', - args: undefined, + en: "Your Account ID", + args: undefined, }, accountIdYoursDescription: { - en: 'This is your Account ID. Other users can scan it to start a conversation with you.', - args: undefined, + en: "This is your Account ID. Other users can scan it to start a conversation with you.", + args: undefined, }, actualSize: { - en: 'Actual Size', - args: undefined, + en: "Actual Size", + args: undefined, }, add: { - en: 'Add', - args: undefined, + en: "Add", + args: undefined, }, addAdmins: { - en: 'Add Admins', - args: undefined, + en: "Add Admins", + args: undefined, }, addAdminsDescription: { - en: 'Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time.', - args: undefined, + en: "Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time.", + args: undefined, }, adminCannotBeRemoved: { - en: 'Admins cannot be removed.', - args: undefined, + en: "Admins cannot be removed.", + args: undefined, }, adminMorePromotedToAdmin: { - en: '{name} and {count} others were promoted to Admin.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others were promoted to Admin.", + args: {name: "string", count: "number"} }, adminPromote: { - en: 'Promote Admins', - args: undefined, + en: "Promote Admins", + args: undefined, }, adminPromoteDescription: { - en: 'Are you sure you want to promote {name} to admin? Admins cannot be removed.', - args: { name: 'string' }, + en: "Are you sure you want to promote {name} to admin? Admins cannot be removed.", + args: {name: "string"} }, adminPromoteMoreDescription: { - en: 'Are you sure you want to promote {name} and {count} others to admin? Admins cannot be removed.', - args: { name: 'string', count: 'number' }, + en: "Are you sure you want to promote {name} and {count} others to admin? Admins cannot be removed.", + args: {name: "string", count: "number"} }, adminPromoteToAdmin: { - en: 'Promote to Admin', - args: undefined, + en: "Promote to Admin", + args: undefined, }, adminPromoteTwoDescription: { - en: 'Are you sure you want to promote {name} and {other_name} to admin? Admins cannot be removed.', - args: { name: 'string', other_name: 'string' }, + en: "Are you sure you want to promote {name} and {other_name} to admin? Admins cannot be removed.", + args: {name: "string", other_name: "string"} }, adminPromotedToAdmin: { - en: '{name} was promoted to Admin.', - args: { name: 'string' }, + en: "{name} was promoted to Admin.", + args: {name: "string"} }, adminPromotionFailed: { - en: 'Admin promotion failed', - args: undefined, + en: "Admin promotion failed", + args: undefined, }, adminPromotionFailedDescription: { - en: 'Failed to promote {name} in {group_name}', - args: { name: 'string', group_name: 'string' }, + en: "Failed to promote {name} in {group_name}", + args: {name: "string", group_name: "string"} }, adminPromotionFailedDescriptionMultiple: { - en: 'Failed to promote {name} and {count} others in {group_name}', - args: { name: 'string', count: 'number', group_name: 'string' }, + en: "Failed to promote {name} and {count} others in {group_name}", + args: {name: "string", count: "number", group_name: "string"} }, adminPromotionFailedDescriptionTwo: { - en: 'Failed to promote {name} and {other_name} in {group_name}', - args: { name: 'string', other_name: 'string', group_name: 'string' }, + en: "Failed to promote {name} and {other_name} in {group_name}", + args: {name: "string", other_name: "string", group_name: "string"} }, adminPromotionNotSent: { - en: 'Promotion not sent', - args: undefined, + en: "Promotion not sent", + args: undefined, }, adminPromotionSent: { - en: 'Admin promotion sent', - args: undefined, + en: "Admin promotion sent", + args: undefined, }, adminPromotionStatusUnknown: { - en: 'Promotion status unknown', - args: undefined, + en: "Promotion status unknown", + args: undefined, }, adminRemove: { - en: 'Remove Admins', - args: undefined, + en: "Remove Admins", + args: undefined, }, adminRemoveAsAdmin: { - en: 'Remove as Admin', - args: undefined, + en: "Remove as Admin", + args: undefined, }, adminRemoveCommunityNone: { - en: 'There are no Admins in this Community.', - args: undefined, + en: "There are no Admins in this Community.", + args: undefined, }, adminRemoveFailed: { - en: 'Failed to remove {name} as Admin.', - args: { name: 'string' }, + en: "Failed to remove {name} as Admin.", + args: {name: "string"} }, adminRemoveFailedMultiple: { - en: 'Failed to remove {name} and {count} others as Admin.', - args: { name: 'string', count: 'number' }, + en: "Failed to remove {name} and {count} others as Admin.", + args: {name: "string", count: "number"} }, adminRemoveFailedOther: { - en: 'Failed to remove {name} and {other_name} as Admin.', - args: { name: 'string', other_name: 'string' }, + en: "Failed to remove {name} and {other_name} as Admin.", + args: {name: "string", other_name: "string"} }, adminRemovedUser: { - en: '{name} was removed as Admin.', - args: { name: 'string' }, + en: "{name} was removed as Admin.", + args: {name: "string"} }, adminRemovedUserMultiple: { - en: '{name} and {count} others were removed as Admin.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others were removed as Admin.", + args: {name: "string", count: "number"} }, adminRemovedUserOther: { - en: '{name} and {other_name} were removed as Admin.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} were removed as Admin.", + args: {name: "string", other_name: "string"} }, adminSettings: { - en: 'Admin Settings', - args: undefined, + en: "Admin Settings", + args: undefined, }, adminTwoPromotedToAdmin: { - en: '{name} and {other_name} were promoted to Admin.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} were promoted to Admin.", + args: {name: "string", other_name: "string"} }, andMore: { - en: '+{count}', - args: { count: 'number' }, + en: "+{count}", + args: {count: "number"} }, anonymous: { - en: 'Anonymous', - args: undefined, + en: "Anonymous", + args: undefined, }, appIcon: { - en: 'App Icon', - args: undefined, + en: "App Icon", + args: undefined, }, appIconAndNameChange: { - en: 'Change App Icon and Name', - args: undefined, + en: "Change App Icon and Name", + args: undefined, }, appIconAndNameChangeConfirmation: { - en: 'Changing the app icon and name requires Session to be closed. Notifications will continue to use the default Session icon and name.', - args: undefined, + en: "Changing the app icon and name requires Session to be closed. Notifications will continue to use the default Session icon and name.", + args: undefined, }, appIconAndNameDescription: { - en: 'Alternate app icon and name is displayed on home screen and app drawer.', - args: undefined, + en: "Alternate app icon and name is displayed on home screen and app drawer.", + args: undefined, }, appIconAndNameSelectionDescription: { - en: 'The selected app icon and name is displayed on the home screen and app drawer.', - args: undefined, + en: "The selected app icon and name is displayed on the home screen and app drawer.", + args: undefined, }, appIconAndNameSelectionTitle: { - en: 'Icon and name', - args: undefined, + en: "Icon and name", + args: undefined, }, appIconDescription: { - en: "Alternate app icon is displayed on home screen and app library. App name will still appear as 'Session'.", - args: undefined, + en: "Alternate app icon is displayed on home screen and app library. App name will still appear as 'Session'.", + args: undefined, }, appIconEnableIcon: { - en: 'Use alternate app icon', - args: undefined, + en: "Use alternate app icon", + args: undefined, }, appIconEnableIconAndName: { - en: 'Use alternate app icon and name', - args: undefined, + en: "Use alternate app icon and name", + args: undefined, }, appIconSelect: { - en: 'Select alternate app icon', - args: undefined, + en: "Select alternate app icon", + args: undefined, }, appIconSelectionTitle: { - en: 'Icon', - args: undefined, + en: "Icon", + args: undefined, }, appNameCalculator: { - en: 'Calculator', - args: undefined, + en: "Calculator", + args: undefined, }, appNameMeetingSE: { - en: 'MeetingSE', - args: undefined, + en: "MeetingSE", + args: undefined, }, appNameNews: { - en: 'News', - args: undefined, + en: "News", + args: undefined, }, appNameNotes: { - en: 'Notes', - args: undefined, + en: "Notes", + args: undefined, }, appNameStocks: { - en: 'Stocks', - args: undefined, + en: "Stocks", + args: undefined, }, appNameWeather: { - en: 'Weather', - args: undefined, + en: "Weather", + args: undefined, }, appearanceAutoDarkMode: { - en: 'Auto dark-mode', - args: undefined, + en: "Auto dark-mode", + args: undefined, }, appearanceHideMenuBar: { - en: 'Hide Menu Bar', - args: undefined, + en: "Hide Menu Bar", + args: undefined, }, appearanceLanguage: { - en: 'Language', - args: undefined, + en: "Language", + args: undefined, }, appearanceLanguageDescription: { - en: 'Choose your language setting for Session. Session will restart when you change your language setting.', - args: undefined, + en: "Choose your language setting for Session. Session will restart when you change your language setting.", + args: undefined, }, appearancePreview1: { - en: 'How are you?', - args: undefined, + en: "How are you?", + args: undefined, }, appearancePreview2: { - en: "I'm good thanks, you?", - args: undefined, + en: "I'm good thanks, you?", + args: undefined, }, appearancePreview3: { - en: "I'm doing great, thanks.", - args: undefined, + en: "I'm doing great, thanks.", + args: undefined, }, appearancePrimaryColor: { - en: 'Primary Color', - args: undefined, + en: "Primary Color", + args: undefined, }, appearanceThemes: { - en: 'Themes', - args: undefined, + en: "Themes", + args: undefined, }, appearanceThemesClassicDark: { - en: 'Classic Dark', - args: undefined, + en: "Classic Dark", + args: undefined, }, appearanceThemesClassicLight: { - en: 'Classic Light', - args: undefined, + en: "Classic Light", + args: undefined, }, appearanceThemesOceanDark: { - en: 'Ocean Dark', - args: undefined, + en: "Ocean Dark", + args: undefined, }, appearanceThemesOceanLight: { - en: 'Ocean Light', - args: undefined, + en: "Ocean Light", + args: undefined, }, appearanceZoom: { - en: 'Zoom', - args: undefined, + en: "Zoom", + args: undefined, }, appearanceZoomIn: { - en: 'Zoom In', - args: undefined, + en: "Zoom In", + args: undefined, }, appearanceZoomOut: { - en: 'Zoom Out', - args: undefined, + en: "Zoom Out", + args: undefined, }, attachment: { - en: 'Attachment', - args: undefined, + en: "Attachment", + args: undefined, }, attachments: { - en: 'Attachments', - args: undefined, + en: "Attachments", + args: undefined, }, attachmentsAdd: { - en: 'Add attachment', - args: undefined, + en: "Add attachment", + args: undefined, }, attachmentsAlbumUnnamed: { - en: 'Unnamed Album', - args: undefined, + en: "Unnamed Album", + args: undefined, }, attachmentsAutoDownload: { - en: 'Auto-download Attachments', - args: undefined, + en: "Auto-download Attachments", + args: undefined, }, attachmentsAutoDownloadDescription: { - en: 'Automatically download media and files from this chat.', - args: undefined, + en: "Automatically download media and files from this chat.", + args: undefined, }, attachmentsAutoDownloadModalDescription: { - en: 'Would you like to automatically download all files from {conversation_name}?', - args: { conversation_name: 'string' }, + en: "Would you like to automatically download all files from {conversation_name}?", + args: {conversation_name: "string"} }, attachmentsAutoDownloadModalTitle: { - en: 'Auto Download', - args: undefined, + en: "Auto Download", + args: undefined, }, attachmentsClearAll: { - en: 'Clear All Attachments', - args: undefined, + en: "Clear All Attachments", + args: undefined, }, attachmentsClearAllDescription: { - en: 'Are you sure you want to clear all attachments? Messages with attachments will also be deleted.', - args: undefined, + en: "Are you sure you want to clear all attachments? Messages with attachments will also be deleted.", + args: undefined, }, attachmentsClickToDownload: { - en: 'Click to download {file_type}', - args: { file_type: 'string' }, + en: "Click to download {file_type}", + args: {file_type: "string"} }, attachmentsCollapseOptions: { - en: 'Collapse attachment options', - args: undefined, + en: "Collapse attachment options", + args: undefined, }, attachmentsCollecting: { - en: 'Collecting attachments...', - args: undefined, + en: "Collecting attachments...", + args: undefined, }, attachmentsDownload: { - en: 'Download Attachment', - args: undefined, + en: "Download Attachment", + args: undefined, }, attachmentsDuration: { - en: 'Duration:', - args: undefined, + en: "Duration:", + args: undefined, }, attachmentsErrorLoad: { - en: 'Error attaching file', - args: undefined, + en: "Error attaching file", + args: undefined, }, attachmentsErrorMediaSelection: { - en: 'Failed to select attachment', - args: undefined, + en: "Failed to select attachment", + args: undefined, }, attachmentsErrorNoApp: { - en: "Can't find an app to select media.", - args: undefined, + en: "Can't find an app to select media.", + args: undefined, }, attachmentsErrorNotSupported: { - en: 'This file type is not supported.', - args: undefined, + en: "This file type is not supported.", + args: undefined, }, attachmentsErrorNumber: { - en: 'Unable to send more than 32 image and video files at once.', - args: undefined, + en: "Unable to send more than 32 image and video files at once.", + args: undefined, }, attachmentsErrorOpen: { - en: 'Unable to open file.', - args: undefined, + en: "Unable to open file.", + args: undefined, }, attachmentsErrorSending: { - en: 'Error sending file', - args: undefined, + en: "Error sending file", + args: undefined, }, attachmentsErrorSeparate: { - en: 'Please send files as separate messages.', - args: undefined, + en: "Please send files as separate messages.", + args: undefined, }, attachmentsErrorSize: { - en: 'Files must be less than 10MB', - args: undefined, + en: "Files must be less than 10MB", + args: undefined, }, attachmentsErrorTypes: { - en: 'Cannot attach images and video with other file types. Try sending other files in a separate message.', - args: undefined, + en: "Cannot attach images and video with other file types. Try sending other files in a separate message.", + args: undefined, }, attachmentsExpired: { - en: 'Attachment expired', - args: undefined, + en: "Attachment expired", + args: undefined, }, attachmentsFileId: { - en: 'File ID:', - args: undefined, + en: "File ID:", + args: undefined, }, attachmentsFileSize: { - en: 'File Size:', - args: undefined, + en: "File Size:", + args: undefined, }, attachmentsFileType: { - en: 'File Type:', - args: undefined, + en: "File Type:", + args: undefined, }, attachmentsFilesEmpty: { - en: "You don't have any files in this conversation.", - args: undefined, + en: "You don't have any files in this conversation.", + args: undefined, }, attachmentsImageErrorMetadata: { - en: 'Unable to remove metadata from file.', - args: undefined, + en: "Unable to remove metadata from file.", + args: undefined, }, attachmentsLoadingNewer: { - en: 'Loading Newer Media...', - args: undefined, + en: "Loading Newer Media...", + args: undefined, }, attachmentsLoadingNewerFiles: { - en: 'Loading Newer Files...', - args: undefined, + en: "Loading Newer Files...", + args: undefined, }, attachmentsLoadingOlder: { - en: 'Loading Older Media...', - args: undefined, + en: "Loading Older Media...", + args: undefined, }, attachmentsLoadingOlderFiles: { - en: 'Loading Older Files...', - args: undefined, + en: "Loading Older Files...", + args: undefined, }, attachmentsMedia: { - en: '{name} on {date_time}', - args: { name: 'string', date_time: 'string' }, + en: "{name} on {date_time}", + args: {name: "string", date_time: "string"} }, attachmentsMediaEmpty: { - en: "You don't have any media in this conversation.", - args: undefined, + en: "You don't have any media in this conversation.", + args: undefined, }, attachmentsMediaSaved: { - en: 'Media saved by {name}', - args: { name: 'string' }, + en: "Media saved by {name}", + args: {name: "string"} }, attachmentsMoveAndScale: { - en: 'Move and Scale', - args: undefined, + en: "Move and Scale", + args: undefined, }, attachmentsNa: { - en: 'N/A', - args: undefined, + en: "N/A", + args: undefined, }, attachmentsNotification: { - en: '{emoji} Attachment', - args: { emoji: 'string' }, + en: "{emoji} Attachment", + args: {emoji: "string"} }, attachmentsNotificationGroup: { - en: '{author}: {emoji} Attachment', - args: { author: 'string', emoji: 'string' }, + en: "{author}: {emoji} Attachment", + args: {author: "string", emoji: "string"} }, attachmentsResolution: { - en: 'Resolution:', - args: undefined, + en: "Resolution:", + args: undefined, }, attachmentsSaveError: { - en: 'Unable to save file.', - args: undefined, + en: "Unable to save file.", + args: undefined, }, attachmentsSendTo: { - en: 'Send to {name}', - args: { name: 'string' }, + en: "Send to {name}", + args: {name: "string"} }, attachmentsTapToDownload: { - en: 'Tap to download {file_type}', - args: { file_type: 'string' }, + en: "Tap to download {file_type}", + args: {file_type: "string"} }, attachmentsThisMonth: { - en: 'This Month', - args: undefined, + en: "This Month", + args: undefined, }, attachmentsThisWeek: { - en: 'This Week', - args: undefined, + en: "This Week", + args: undefined, }, attachmentsWarning: { - en: 'Attachments you save can be accessed by other apps on your device.', - args: undefined, + en: "Attachments you save can be accessed by other apps on your device.", + args: undefined, }, audio: { - en: 'Audio', - args: undefined, + en: "Audio", + args: undefined, }, audioNoInput: { - en: 'No audio input found', - args: undefined, + en: "No audio input found", + args: undefined, }, audioNoOutput: { - en: 'No audio output found', - args: undefined, + en: "No audio output found", + args: undefined, }, audioUnableToPlay: { - en: 'Unable to play audio file.', - args: undefined, + en: "Unable to play audio file.", + args: undefined, }, audioUnableToRecord: { - en: 'Unable to record audio.', - args: undefined, + en: "Unable to record audio.", + args: undefined, }, authenticateFailed: { - en: 'Authentication Failed', - args: undefined, + en: "Authentication Failed", + args: undefined, }, authenticateFailedTooManyAttempts: { - en: 'Too many failed authentication attempts. Please try again later.', - args: undefined, + en: "Too many failed authentication attempts. Please try again later.", + args: undefined, }, authenticateNotAccessed: { - en: 'Authentication could not be accessed.', - args: undefined, + en: "Authentication could not be accessed.", + args: undefined, }, authenticateToOpen: { - en: 'Authenticate to open Session.', - args: undefined, + en: "Authenticate to open Session.", + args: undefined, }, back: { - en: 'Back', - args: undefined, + en: "Back", + args: undefined, }, banDeleteAll: { - en: 'Ban and Delete All', - args: undefined, + en: "Ban and Delete All", + args: undefined, }, banErrorFailed: { - en: 'Ban failed', - args: undefined, + en: "Ban failed", + args: undefined, }, banUnbanErrorFailed: { - en: 'Unban failed', - args: undefined, + en: "Unban failed", + args: undefined, }, banUnbanUser: { - en: 'Unban User', - args: undefined, + en: "Unban User", + args: undefined, }, banUnbanUserDescription: { - en: 'Enter the Account ID of the user you are unbanning', - args: undefined, + en: "Enter the Account ID of the user you are unbanning", + args: undefined, }, banUnbanUserUnbanned: { - en: 'User unbanned', - args: undefined, + en: "User unbanned", + args: undefined, }, banUser: { - en: 'Ban User', - args: undefined, + en: "Ban User", + args: undefined, }, banUserBanned: { - en: 'User banned', - args: undefined, + en: "User banned", + args: undefined, }, banUserDescription: { - en: 'Enter the Account ID of the user you are banning', - args: undefined, + en: "Enter the Account ID of the user you are banning", + args: undefined, }, block: { - en: 'Block', - args: undefined, + en: "Block", + args: undefined, }, blockBlockedDescription: { - en: 'Unblock this contact to send a message', - args: undefined, + en: "Unblock this contact to send a message", + args: undefined, }, blockBlockedNone: { - en: 'No blocked contacts', - args: undefined, + en: "No blocked contacts", + args: undefined, }, blockBlockedUser: { - en: 'Blocked {name}', - args: { name: 'string' }, + en: "Blocked {name}", + args: {name: "string"} }, blockDescription: { - en: 'Are you sure you want to block {name}? Blocked users cannot send you message requests, group invites or call you.', - args: { name: 'string' }, + en: "Are you sure you want to block {name}? Blocked users cannot send you message requests, group invites or call you.", + args: {name: "string"} }, blockUnblock: { - en: 'Unblock', - args: undefined, + en: "Unblock", + args: undefined, }, blockUnblockName: { - en: 'Are you sure you want to unblock {name}?', - args: { name: 'string' }, + en: "Are you sure you want to unblock {name}?", + args: {name: "string"} }, blockUnblockNameMultiple: { - en: 'Are you sure you want to unblock {name} and {count} others?', - args: { name: 'string', count: 'number' }, + en: "Are you sure you want to unblock {name} and {count} others?", + args: {name: "string", count: "number"} }, blockUnblockNameTwo: { - en: 'Are you sure you want to unblock {name} and 1 other?', - args: { name: 'string' }, + en: "Are you sure you want to unblock {name} and 1 other?", + args: {name: "string"} }, blockUnblockedUser: { - en: 'Unblocked {name}', - args: { name: 'string' }, + en: "Unblocked {name}", + args: {name: "string"} }, call: { - en: 'Call', - args: undefined, + en: "Call", + args: undefined, }, callsCalledYou: { - en: '{name} called you', - args: { name: 'string' }, + en: "{name} called you", + args: {name: "string"} }, callsCannotStart: { - en: 'You cannot start a new call. Finish your current call first.', - args: undefined, + en: "You cannot start a new call. Finish your current call first.", + args: undefined, }, callsConnecting: { - en: 'Connecting...', - args: undefined, + en: "Connecting...", + args: undefined, }, callsEnd: { - en: 'End call', - args: undefined, + en: "End call", + args: undefined, }, callsEnded: { - en: 'Call Ended', - args: undefined, + en: "Call Ended", + args: undefined, }, callsErrorAnswer: { - en: 'Failed to answer call', - args: undefined, + en: "Failed to answer call", + args: undefined, }, callsErrorStart: { - en: 'Failed to start call', - args: undefined, + en: "Failed to start call", + args: undefined, }, callsInProgress: { - en: 'Call in progress', - args: undefined, + en: "Call in progress", + args: undefined, }, callsIncoming: { - en: 'Incoming call from {name}', - args: { name: 'string' }, + en: "Incoming call from {name}", + args: {name: "string"} }, callsIncomingUnknown: { - en: 'Incoming call', - args: undefined, + en: "Incoming call", + args: undefined, }, callsMicrophonePermissionsRequired: { - en: "You missed a call from {name} because you haven't granted microphone access.", - args: { name: 'string' }, + en: "You missed a call from {name} because you haven't granted microphone access.", + args: {name: "string"} }, callsMissed: { - en: 'Missed Call', - args: undefined, + en: "Missed Call", + args: undefined, }, callsMissedCallFrom: { - en: 'Missed call from {name}', - args: { name: 'string' }, + en: "Missed call from {name}", + args: {name: "string"} }, callsNotificationsRequired: { - en: 'Voice and Video Calls require notifications to be enabled in your device system settings.', - args: undefined, + en: "Voice and Video Calls require notifications to be enabled in your device system settings.", + args: undefined, }, callsPermissionsRequired: { - en: 'Call Permissions Required', - args: undefined, + en: "Call Permissions Required", + args: undefined, }, callsPermissionsRequiredDescription: { - en: 'You can enable the "Voice and Video Calls" permission in Privacy Settings.', - args: undefined, + en: "You can enable the \"Voice and Video Calls\" permission in Privacy Settings.", + args: undefined, }, callsPermissionsRequiredDescription1: { - en: 'You can enable the "Voice and Video Calls" permission in Permissions Settings.', - args: undefined, + en: "You can enable the \"Voice and Video Calls\" permission in Permissions Settings.", + args: undefined, }, callsReconnecting: { - en: 'Reconnecting…', - args: undefined, + en: "Reconnecting…", + args: undefined, }, callsRinging: { - en: 'Ringing...', - args: undefined, + en: "Ringing...", + args: undefined, }, callsSessionCall: { - en: 'Session Call', - args: undefined, + en: "Session Call", + args: undefined, }, callsSettings: { - en: 'Calls (Beta)', - args: undefined, + en: "Calls (Beta)", + args: undefined, }, callsVoiceAndVideo: { - en: 'Voice and Video Calls', - args: undefined, + en: "Voice and Video Calls", + args: undefined, }, callsVoiceAndVideoBeta: { - en: 'Voice and Video Calls (Beta)', - args: undefined, + en: "Voice and Video Calls (Beta)", + args: undefined, }, callsVoiceAndVideoModalDescription: { - en: 'Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.', - args: undefined, + en: "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.", + args: undefined, }, callsVoiceAndVideoToggleDescription: { - en: 'Enables voice and video calls to and from other users.', - args: undefined, + en: "Enables voice and video calls to and from other users.", + args: undefined, }, callsYouCalled: { - en: 'You called {name}', - args: { name: 'string' }, + en: "You called {name}", + args: {name: "string"} }, callsYouMissedCallPermissions: { - en: "You missed a call from {name} because you haven't enabled Voice and Video Calls in Privacy Settings.", - args: { name: 'string' }, + en: "You missed a call from {name} because you haven't enabled Voice and Video Calls in Privacy Settings.", + args: {name: "string"} }, cameraErrorNotFound: { - en: 'No camera found', - args: undefined, + en: "No camera found", + args: undefined, }, cameraErrorUnavailable: { - en: 'Camera unavailable.', - args: undefined, + en: "Camera unavailable.", + args: undefined, }, cameraGrantAccess: { - en: 'Grant Camera Access', - args: undefined, + en: "Grant Camera Access", + args: undefined, }, cameraGrantAccessDenied: { - en: 'Session needs camera access to take photos and videos, but it has been permanently denied. Please continue to app settings, select "Permissions", and enable "Camera".', - args: undefined, + en: "Session needs camera access to take photos and videos, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".", + args: undefined, }, cameraGrantAccessDescription: { - en: 'Session needs camera access to take photos and videos, or scan QR codes.', - args: undefined, + en: "Session needs camera access to take photos and videos, or scan QR codes.", + args: undefined, }, cameraGrantAccessQr: { - en: 'Session needs camera access to scan QR codes', - args: undefined, + en: "Session needs camera access to scan QR codes", + args: undefined, }, cancel: { - en: 'Cancel', - args: undefined, + en: "Cancel", + args: undefined, }, changePasswordFail: { - en: 'Failed to change password', - args: undefined, + en: "Failed to change password", + args: undefined, }, clear: { - en: 'Clear', - args: undefined, + en: "Clear", + args: undefined, }, clearAll: { - en: 'Clear All', - args: undefined, + en: "Clear All", + args: undefined, }, clearDataAll: { - en: 'Clear All Data', - args: undefined, + en: "Clear All Data", + args: undefined, }, clearDataAllDescription: { - en: 'This will permanently delete your messages and contacts. Would you like to clear this device only, or delete your data from the network as well?', - args: undefined, + en: "This will permanently delete your messages and contacts. Would you like to clear this device only, or delete your data from the network as well?", + args: undefined, }, clearDataError: { - en: 'Data Not Deleted', - args: undefined, + en: "Data Not Deleted", + args: undefined, }, clearDataErrorDescriptionGeneric: { - en: 'An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead?', - args: undefined, + en: "An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead?", + args: undefined, }, clearDevice: { - en: 'Clear Device', - args: undefined, + en: "Clear Device", + args: undefined, }, clearDeviceAndNetwork: { - en: 'Clear device and network', - args: undefined, + en: "Clear device and network", + args: undefined, }, clearDeviceAndNetworkConfirm: { - en: 'Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.', - args: undefined, + en: "Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.", + args: undefined, }, clearDeviceDescription: { - en: 'Are you sure you want to clear your device?', - args: undefined, + en: "Are you sure you want to clear your device?", + args: undefined, }, clearDeviceOnly: { - en: 'Clear device only', - args: undefined, + en: "Clear device only", + args: undefined, }, clearDeviceRestart: { - en: 'Clear Device and Restart', - args: undefined, + en: "Clear Device and Restart", + args: undefined, }, clearDeviceRestore: { - en: 'Clear Device and Restore', - args: undefined, + en: "Clear Device and Restore", + args: undefined, }, clearMessages: { - en: 'Clear All Messages', - args: undefined, + en: "Clear All Messages", + args: undefined, }, clearMessagesChatDescription: { - en: 'Are you sure you want to clear all messages from your conversation with {name} from your device?', - args: { name: 'string' }, + en: "Are you sure you want to clear all messages from your conversation with {name} from your device?", + args: {name: "string"} }, clearMessagesChatDescriptionUpdated: { - en: 'Are you sure you want to clear all messages from your conversation with {name} on this device?', - args: { name: 'string' }, + en: "Are you sure you want to clear all messages from your conversation with {name} on this device?", + args: {name: "string"} }, clearMessagesCommunity: { - en: 'Are you sure you want to clear all {community_name} messages from your device?', - args: { community_name: 'string' }, + en: "Are you sure you want to clear all {community_name} messages from your device?", + args: {community_name: "string"} }, clearMessagesCommunityUpdated: { - en: 'Are you sure you want to clear all messages from {community_name} on this device?', - args: { community_name: 'string' }, + en: "Are you sure you want to clear all messages from {community_name} on this device?", + args: {community_name: "string"} }, clearMessagesForEveryone: { - en: 'Clear for everyone', - args: undefined, + en: "Clear for everyone", + args: undefined, }, clearMessagesForMe: { - en: 'Clear for me', - args: undefined, + en: "Clear for me", + args: undefined, }, clearMessagesGroupAdminDescription: { - en: 'Are you sure you want to clear all {group_name} messages?', - args: { group_name: 'string' }, + en: "Are you sure you want to clear all {group_name} messages?", + args: {group_name: "string"} }, clearMessagesGroupAdminDescriptionUpdated: { - en: 'Are you sure you want to clear all messages from {group_name}?', - args: { group_name: 'string' }, + en: "Are you sure you want to clear all messages from {group_name}?", + args: {group_name: "string"} }, clearMessagesGroupDescription: { - en: 'Are you sure you want to clear all {group_name} messages from your device?', - args: { group_name: 'string' }, + en: "Are you sure you want to clear all {group_name} messages from your device?", + args: {group_name: "string"} }, clearMessagesGroupDescriptionUpdated: { - en: 'Are you sure you want to clear all messages from {group_name} on this device?', - args: { group_name: 'string' }, + en: "Are you sure you want to clear all messages from {group_name} on this device?", + args: {group_name: "string"} }, clearMessagesNoteToSelfDescription: { - en: 'Are you sure you want to clear all Note to Self messages from your device?', - args: undefined, + en: "Are you sure you want to clear all Note to Self messages from your device?", + args: undefined, }, clearMessagesNoteToSelfDescriptionUpdated: { - en: 'Are you sure you want to clear all Note to Self messages on this device?', - args: undefined, + en: "Are you sure you want to clear all Note to Self messages on this device?", + args: undefined, }, clearOnThisDevice: { - en: 'Clear on this device', - args: undefined, + en: "Clear on this device", + args: undefined, }, close: { - en: 'Close', - args: undefined, + en: "Close", + args: undefined, }, closeApp: { - en: 'Close App', - args: undefined, + en: "Close App", + args: undefined, }, closeWindow: { - en: 'Close Window', - args: undefined, + en: "Close Window", + args: undefined, }, commitHashDesktop: { - en: 'Commit Hash: {hash}', - args: { hash: 'string' }, + en: "Commit Hash: {hash}", + args: {hash: "string"} }, communityBanDeleteDescription: { - en: 'This will ban the selected user from this Community and delete all their messages. Are you sure you want to continue?', - args: undefined, + en: "This will ban the selected user from this Community and delete all their messages. Are you sure you want to continue?", + args: undefined, }, communityBanDescription: { - en: 'This will ban the selected user from this Community. Are you sure you want to continue?', - args: undefined, + en: "This will ban the selected user from this Community. Are you sure you want to continue?", + args: undefined, }, communityEnterUrl: { - en: 'Enter Community URL', - args: undefined, + en: "Enter Community URL", + args: undefined, }, communityEnterUrlErrorInvalid: { - en: 'Invalid URL', - args: undefined, + en: "Invalid URL", + args: undefined, }, communityEnterUrlErrorInvalidDescription: { - en: 'Please check the Community URL and try again.', - args: undefined, + en: "Please check the Community URL and try again.", + args: undefined, }, communityError: { - en: 'Community Error', - args: undefined, + en: "Community Error", + args: undefined, }, communityErrorDescription: { - en: 'Oops, an error occurred. Please try again later.', - args: undefined, + en: "Oops, an error occurred. Please try again later.", + args: undefined, }, communityInvitation: { - en: 'Community Invitation', - args: undefined, + en: "Community Invitation", + args: undefined, }, communityJoin: { - en: 'Join Community', - args: undefined, + en: "Join Community", + args: undefined, }, communityJoinDescription: { - en: 'Are you sure you want to join {community_name}?', - args: { community_name: 'string' }, + en: "Are you sure you want to join {community_name}?", + args: {community_name: "string"} }, communityJoinError: { - en: 'Failed to join community', - args: undefined, + en: "Failed to join community", + args: undefined, }, communityJoinOfficial: { - en: 'Or join one of these...', - args: undefined, + en: "Or join one of these...", + args: undefined, }, communityJoined: { - en: 'Joined Community', - args: undefined, + en: "Joined Community", + args: undefined, }, communityJoinedAlready: { - en: 'You are already a member of this community.', - args: undefined, + en: "You are already a member of this community.", + args: undefined, }, communityLeave: { - en: 'Leave Community', - args: undefined, + en: "Leave Community", + args: undefined, }, communityLeaveError: { - en: 'Failed to leave {community_name}', - args: { community_name: 'string' }, + en: "Failed to leave {community_name}", + args: {community_name: "string"} }, communityUnknown: { - en: 'Unknown Community', - args: undefined, + en: "Unknown Community", + args: undefined, }, communityUrl: { - en: 'Community URL', - args: undefined, + en: "Community URL", + args: undefined, }, communityUrlCopy: { - en: 'Copy Community URL', - args: undefined, + en: "Copy Community URL", + args: undefined, }, confirm: { - en: 'Confirm', - args: undefined, + en: "Confirm", + args: undefined, }, contactContacts: { - en: 'Contacts', - args: undefined, + en: "Contacts", + args: undefined, }, contactDelete: { - en: 'Delete Contact', - args: undefined, + en: "Delete Contact", + args: undefined, }, contactDeleteDescription: { - en: 'Are you sure you want to delete {name} from your contacts? New messages from {name} will arrive as a message request.', - args: { name: 'string' }, + en: "Are you sure you want to delete {name} from your contacts? New messages from {name} will arrive as a message request.", + args: {name: "string"} }, contactNone: { - en: "You don't have any contacts yet", - args: undefined, + en: "You don't have any contacts yet", + args: undefined, }, contactSelect: { - en: 'Select Contacts', - args: undefined, + en: "Select Contacts", + args: undefined, }, contactUserDetails: { - en: 'User Details', - args: undefined, + en: "User Details", + args: undefined, }, contentDescriptionCamera: { - en: 'Camera', - args: undefined, + en: "Camera", + args: undefined, }, contentDescriptionChooseConversationType: { - en: 'Choose an action to start a conversation', - args: undefined, + en: "Choose an action to start a conversation", + args: undefined, }, contentDescriptionMediaMessage: { - en: 'Media message', - args: undefined, + en: "Media message", + args: undefined, }, contentDescriptionMessageComposition: { - en: 'Message composition', - args: undefined, + en: "Message composition", + args: undefined, }, contentDescriptionQuoteThumbnail: { - en: 'Thumbnail of image from quoted message', - args: undefined, + en: "Thumbnail of image from quoted message", + args: undefined, }, contentDescriptionStartConversation: { - en: 'Create a conversation with a new contact', - args: undefined, + en: "Create a conversation with a new contact", + args: undefined, }, conversationsAddToHome: { - en: 'Add to home screen', - args: undefined, + en: "Add to home screen", + args: undefined, }, conversationsAddedToHome: { - en: 'Added to home screen', - args: undefined, + en: "Added to home screen", + args: undefined, }, conversationsAudioMessages: { - en: 'Audio Messages', - args: undefined, + en: "Audio Messages", + args: undefined, }, conversationsAutoplayAudioMessage: { - en: 'Autoplay Audio Messages', - args: undefined, + en: "Autoplay Audio Messages", + args: undefined, }, conversationsAutoplayAudioMessageDescription: { - en: 'Autoplay consecutively sent audio messages.', - args: undefined, + en: "Autoplay consecutively sent audio messages.", + args: undefined, }, conversationsBlockedContacts: { - en: 'Blocked Contacts', - args: undefined, + en: "Blocked Contacts", + args: undefined, }, conversationsCommunities: { - en: 'Communities', - args: undefined, + en: "Communities", + args: undefined, }, conversationsDelete: { - en: 'Delete Conversation', - args: undefined, + en: "Delete Conversation", + args: undefined, }, conversationsDeleteDescription: { - en: 'Are you sure you want to delete your conversation with {name}? New messages from {name} will start a new conversation.', - args: { name: 'string' }, + en: "Are you sure you want to delete your conversation with {name}? New messages from {name} will start a new conversation.", + args: {name: "string"} }, conversationsDeleted: { - en: 'Conversation deleted', - args: undefined, + en: "Conversation deleted", + args: undefined, }, conversationsEmpty: { - en: 'There are no messages in {conversation_name}.', - args: { conversation_name: 'string' }, + en: "There are no messages in {conversation_name}.", + args: {conversation_name: "string"} }, conversationsEnter: { - en: 'Enter Key', - args: undefined, + en: "Enter Key", + args: undefined, }, conversationsEnterDescription: { - en: 'Function of the enter key when typing in a conversation.', - args: undefined, + en: "Function of the enter key when typing in a conversation.", + args: undefined, }, conversationsEnterNewLine: { - en: 'SHIFT + ENTER sends a message, ENTER starts a new line', - args: undefined, + en: "SHIFT + ENTER sends a message, ENTER starts a new line", + args: undefined, }, conversationsEnterSends: { - en: 'ENTER sends a message, SHIFT + ENTER starts a new line', - args: undefined, + en: "ENTER sends a message, SHIFT + ENTER starts a new line", + args: undefined, }, conversationsGroups: { - en: 'Groups', - args: undefined, + en: "Groups", + args: undefined, }, conversationsMessageTrimming: { - en: 'Message Trimming', - args: undefined, + en: "Message Trimming", + args: undefined, }, conversationsMessageTrimmingTrimCommunities: { - en: 'Trim Communities', - args: undefined, + en: "Trim Communities", + args: undefined, }, conversationsMessageTrimmingTrimCommunitiesDescription: { - en: 'Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages.', - args: undefined, + en: "Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages.", + args: undefined, }, conversationsNew: { - en: 'New Conversation', - args: undefined, + en: "New Conversation", + args: undefined, }, conversationsNone: { - en: "You don't have any conversations yet", - args: undefined, + en: "You don't have any conversations yet", + args: undefined, }, conversationsSendWithEnterKey: { - en: 'Send with Enter Key', - args: undefined, + en: "Send with Enter Key", + args: undefined, }, conversationsSendWithEnterKeyDescription: { - en: 'Tapping the Enter Key will send message instead of starting a new line.', - args: undefined, + en: "Tapping the Enter Key will send message instead of starting a new line.", + args: undefined, }, conversationsSettingsAllMedia: { - en: 'All Media', - args: undefined, + en: "All Media", + args: undefined, }, conversationsSpellCheck: { - en: 'Spell Check', - args: undefined, + en: "Spell Check", + args: undefined, }, conversationsSpellCheckDescription: { - en: 'Enable spell check when typing messages.', - args: undefined, + en: "Enable spell check when typing messages.", + args: undefined, }, conversationsStart: { - en: 'Start Conversation', - args: undefined, + en: "Start Conversation", + args: undefined, }, copied: { - en: 'Copied', - args: undefined, + en: "Copied", + args: undefined, }, copy: { - en: 'Copy', - args: undefined, + en: "Copy", + args: undefined, }, create: { - en: 'Create', - args: undefined, + en: "Create", + args: undefined, }, creatingCall: { - en: 'Creating Call', - args: undefined, + en: "Creating Call", + args: undefined, }, cut: { - en: 'Cut', - args: undefined, + en: "Cut", + args: undefined, }, databaseErrorClearDataWarning: { - en: 'Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?', - args: undefined, + en: "Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?", + args: undefined, }, databaseErrorGeneric: { - en: 'A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.', - args: undefined, + en: "A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.", + args: undefined, }, databaseErrorRestoreDataWarning: { - en: 'Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?', - args: undefined, + en: "Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?", + args: undefined, }, databaseErrorTimeout: { - en: "We've noticed Session is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session.", - args: undefined, + en: "We've noticed Session is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session.", + args: undefined, }, databaseErrorUpdate: { - en: 'Your app database is incompatible with this version of Session. Reinstall the app and restore your account to generate a new database and continue using Session.

Warning: This will result in the loss of all messages and attachments older than two weeks.', - args: undefined, + en: "Your app database is incompatible with this version of Session. Reinstall the app and restore your account to generate a new database and continue using Session.

Warning: This will result in the loss of all messages and attachments older than two weeks.", + args: undefined, }, databaseOptimizing: { - en: 'Optimizing Database', - args: undefined, + en: "Optimizing Database", + args: undefined, }, debugLog: { - en: 'Debug Log', - args: undefined, + en: "Debug Log", + args: undefined, }, decline: { - en: 'Decline', - args: undefined, + en: "Decline", + args: undefined, }, delete: { - en: 'Delete', - args: undefined, + en: "Delete", + args: undefined, }, deleteAfterGroupFirstReleaseConfigOutdated: { - en: 'Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.', - args: undefined, + en: "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.", + args: undefined, }, deleteAfterGroupPR1BlockThisUser: { - en: 'Block This User', - args: undefined, + en: "Block This User", + args: undefined, }, deleteAfterGroupPR1BlockUser: { - en: 'Block User', - args: undefined, + en: "Block User", + args: undefined, }, deleteAfterGroupPR1GroupSettings: { - en: 'Group Settings', - args: undefined, + en: "Group Settings", + args: undefined, }, deleteAfterGroupPR1MentionsOnly: { - en: 'Notify for Mentions Only', - args: undefined, + en: "Notify for Mentions Only", + args: undefined, }, deleteAfterGroupPR1MentionsOnlyDescription: { - en: "When enabled, you'll only be notified for messages mentioning you.", - args: undefined, + en: "When enabled, you'll only be notified for messages mentioning you.", + args: undefined, }, deleteAfterGroupPR1MessageSound: { - en: 'Message Sound', - args: undefined, + en: "Message Sound", + args: undefined, }, deleteAfterGroupPR3DeleteMessagesConfirmation: { - en: 'Permanently delete the messages in this conversation?', - args: undefined, + en: "Permanently delete the messages in this conversation?", + args: undefined, }, deleteAfterGroupPR3GroupErrorLeave: { - en: "Can't leave while adding or removing other members.", - args: undefined, + en: "Can't leave while adding or removing other members.", + args: undefined, }, deleteAfterLegacyDisappearingMessagesLegacy: { - en: 'Legacy', - args: undefined, + en: "Legacy", + args: undefined, }, deleteAfterLegacyDisappearingMessagesOriginal: { - en: 'Original version of disappearing messages.', - args: undefined, + en: "Original version of disappearing messages.", + args: undefined, }, deleteAfterLegacyDisappearingMessagesTheyChangedTimer: { - en: '{name} set the disappearing message timer to {time}', - args: { name: 'string', time: 'string' }, + en: "{name} set the disappearing message timer to {time}", + args: {name: "string", time: "string"} }, deleteAfterLegacyGroupsGroupCreation: { - en: 'Please wait while the group is created...', - args: undefined, + en: "Please wait while the group is created...", + args: undefined, }, deleteAfterLegacyGroupsGroupUpdateErrorTitle: { - en: 'Failed to Update Group', - args: undefined, + en: "Failed to Update Group", + args: undefined, }, deleteAfterMessageDeletionStandardisationMessageDeletionForbidden: { - en: 'You don’t have permission to delete others’ messages', - args: undefined, + en: "You don’t have permission to delete others’ messages", + args: undefined, }, deleteContactDescription: { - en: 'Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request.', - args: { name: 'string' }, + en: "Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request.", + args: {name: "string"} }, deleteConversationDescription: { - en: 'Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments.', - args: { name: 'string' }, + en: "Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments.", + args: {name: "string"} }, deleteMessageDeletedGlobally: { - en: 'This message was deleted', - args: undefined, + en: "This message was deleted", + args: undefined, }, deleteMessageDeletedLocally: { - en: 'This message was deleted on this device', - args: undefined, + en: "This message was deleted on this device", + args: undefined, }, deleteMessageDescriptionEveryone: { - en: 'Are you sure you want to delete this message for everyone?', - args: undefined, + en: "Are you sure you want to delete this message for everyone?", + args: undefined, }, deleteMessageDeviceOnly: { - en: 'Delete on this device only', - args: undefined, + en: "Delete on this device only", + args: undefined, }, deleteMessageDevicesAll: { - en: 'Delete on all my devices', - args: undefined, + en: "Delete on all my devices", + args: undefined, }, deleteMessageEveryone: { - en: 'Delete for everyone', - args: undefined, + en: "Delete for everyone", + args: undefined, }, deleteMessagesDescriptionEveryone: { - en: 'Are you sure you want to delete these messages for everyone?', - args: undefined, + en: "Are you sure you want to delete these messages for everyone?", + args: undefined, }, deleting: { - en: 'Deleting', - args: undefined, + en: "Deleting", + args: undefined, }, developerToolsToggle: { - en: 'Toggle Developer Tools', - args: undefined, + en: "Toggle Developer Tools", + args: undefined, }, dictationStart: { - en: 'Start Dictation...', - args: undefined, + en: "Start Dictation...", + args: undefined, }, disappearingMessages: { - en: 'Disappearing Messages', - args: undefined, + en: "Disappearing Messages", + args: undefined, }, disappearingMessagesCountdownBig: { - en: 'Message will delete in {time_large}', - args: { time_large: 'string' }, + en: "Message will delete in {time_large}", + args: {time_large: "string"} }, disappearingMessagesCountdownBigMobile: { - en: 'Auto-deletes in {time_large}', - args: { time_large: 'string' }, + en: "Auto-deletes in {time_large}", + args: {time_large: "string"} }, disappearingMessagesCountdownBigSmall: { - en: 'Message will delete in {time_large} {time_small}', - args: { time_large: 'string', time_small: 'string' }, + en: "Message will delete in {time_large} {time_small}", + args: {time_large: "string", time_small: "string"} }, disappearingMessagesCountdownBigSmallMobile: { - en: 'Auto-deletes in {time_large} {time_small}', - args: { time_large: 'string', time_small: 'string' }, + en: "Auto-deletes in {time_large} {time_small}", + args: {time_large: "string", time_small: "string"} }, disappearingMessagesDeleteType: { - en: 'Delete Type', - args: undefined, + en: "Delete Type", + args: undefined, }, disappearingMessagesDescription: { - en: 'This setting applies to everyone in this conversation.', - args: undefined, + en: "This setting applies to everyone in this conversation.", + args: undefined, }, disappearingMessagesDescription1: { - en: 'This setting applies to messages you send in this conversation.', - args: undefined, + en: "This setting applies to messages you send in this conversation.", + args: undefined, }, disappearingMessagesDescriptionGroup: { - en: 'This setting applies to everyone in this conversation.
Only group admins can change this setting.', - args: undefined, + en: "This setting applies to everyone in this conversation.
Only group admins can change this setting.", + args: undefined, }, disappearingMessagesDisappear: { - en: 'Disappear After {disappearing_messages_type} - {time}', - args: { disappearing_messages_type: 'string', time: 'string' }, + en: "Disappear After {disappearing_messages_type} - {time}", + args: {disappearing_messages_type: "string", time: "string"} }, disappearingMessagesDisappearAfterRead: { - en: 'Disappear After Read', - args: undefined, + en: "Disappear After Read", + args: undefined, }, disappearingMessagesDisappearAfterReadDescription: { - en: 'Messages delete after they have been read.', - args: undefined, + en: "Messages delete after they have been read.", + args: undefined, }, disappearingMessagesDisappearAfterReadState: { - en: 'Disappear After Read - {time}', - args: { time: 'string' }, + en: "Disappear After Read - {time}", + args: {time: "string"} }, disappearingMessagesDisappearAfterSend: { - en: 'Disappear After Send', - args: undefined, + en: "Disappear After Send", + args: undefined, }, disappearingMessagesDisappearAfterSendDescription: { - en: 'Messages delete after they have been sent.', - args: undefined, + en: "Messages delete after they have been sent.", + args: undefined, }, disappearingMessagesDisappearAfterSendState: { - en: 'Disappear After Send - {time}', - args: { time: 'string' }, + en: "Disappear After Send - {time}", + args: {time: "string"} }, disappearingMessagesFollowSetting: { - en: 'Follow Setting', - args: undefined, + en: "Follow Setting", + args: undefined, }, disappearingMessagesFollowSettingOff: { - en: 'Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?', - args: undefined, + en: "Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?", + args: undefined, }, disappearingMessagesFollowSettingOn: { - en: 'Set your messages to disappear {time} after they have been {disappearing_messages_type}?', - args: { time: 'string', disappearing_messages_type: 'string' }, + en: "Set your messages to disappear {time} after they have been {disappearing_messages_type}?", + args: {time: "string", disappearing_messages_type: "string"} }, disappearingMessagesLegacy: { - en: '{name} is using an outdated client. Disappearing messages may not work as expected.', - args: { name: 'string' }, + en: "{name} is using an outdated client. Disappearing messages may not work as expected.", + args: {name: "string"} }, disappearingMessagesOnlyAdmins: { - en: 'Only group admins can change this setting.', - args: undefined, + en: "Only group admins can change this setting.", + args: undefined, }, disappearingMessagesSent: { - en: 'Sent', - args: undefined, + en: "Sent", + args: undefined, }, disappearingMessagesSet: { - en: '{name} has set messages to disappear {time} after they have been {disappearing_messages_type}.', - args: { name: 'string', time: 'string', disappearing_messages_type: 'string' }, + en: "{name} has set messages to disappear {time} after they have been {disappearing_messages_type}.", + args: {name: "string", time: "string", disappearing_messages_type: "string"} }, disappearingMessagesSetYou: { - en: 'You set messages to disappear {time} after they have been {disappearing_messages_type}.', - args: { time: 'string', disappearing_messages_type: 'string' }, + en: "You set messages to disappear {time} after they have been {disappearing_messages_type}.", + args: {time: "string", disappearing_messages_type: "string"} }, disappearingMessagesTimer: { - en: 'Timer', - args: undefined, + en: "Timer", + args: undefined, }, disappearingMessagesTurnedOff: { - en: '{name} has turned disappearing messages off. Messages they send will no longer disappear.', - args: { name: 'string' }, + en: "{name} has turned disappearing messages off. Messages they send will no longer disappear.", + args: {name: "string"} }, disappearingMessagesTurnedOffGroup: { - en: '{name} has turned disappearing messages off.', - args: { name: 'string' }, + en: "{name} has turned disappearing messages off.", + args: {name: "string"} }, disappearingMessagesTurnedOffYou: { - en: 'You turned off disappearing messages. Messages you send will no longer disappear.', - args: undefined, + en: "You turned off disappearing messages. Messages you send will no longer disappear.", + args: undefined, }, disappearingMessagesTurnedOffYouGroup: { - en: 'You turned off disappearing messages.', - args: undefined, + en: "You turned off disappearing messages.", + args: undefined, }, disappearingMessagesTypeRead: { - en: 'read', - args: undefined, + en: "read", + args: undefined, }, disappearingMessagesTypeSent: { - en: 'sent', - args: undefined, + en: "sent", + args: undefined, }, disappearingMessagesUpdated: { - en: '{admin_name} updated disappearing message settings.', - args: { admin_name: 'string' }, + en: "{admin_name} updated disappearing message settings.", + args: {admin_name: "string"} }, disappearingMessagesUpdatedYou: { - en: 'You updated disappearing message settings.', - args: undefined, + en: "You updated disappearing message settings.", + args: undefined, }, dismiss: { - en: 'Dismiss', - args: undefined, + en: "Dismiss", + args: undefined, }, displayNameDescription: { - en: 'It can be your real name, an alias, or anything else you like — and you can change it at any time.', - args: undefined, + en: "It can be your real name, an alias, or anything else you like — and you can change it at any time.", + args: undefined, }, displayNameEnter: { - en: 'Enter your display name', - args: undefined, + en: "Enter your display name", + args: undefined, }, displayNameErrorDescription: { - en: 'Please enter a display name', - args: undefined, + en: "Please enter a display name", + args: undefined, }, displayNameErrorDescriptionShorter: { - en: 'Please enter a shorter display name', - args: undefined, + en: "Please enter a shorter display name", + args: undefined, }, displayNameErrorNew: { - en: 'We were unable to load your display name. Please enter a new display name to continue.', - args: undefined, + en: "We were unable to load your display name. Please enter a new display name to continue.", + args: undefined, }, displayNameNew: { - en: 'Pick a new display name', - args: undefined, + en: "Pick a new display name", + args: undefined, }, displayNamePick: { - en: 'Pick your display name', - args: undefined, + en: "Pick your display name", + args: undefined, }, displayNameSet: { - en: 'Set Display Name', - args: undefined, + en: "Set Display Name", + args: undefined, }, displayNameVisible: { - en: 'Your Display Name is visible to users, groups and communities you interact with.', - args: undefined, + en: "Your Display Name is visible to users, groups and communities you interact with.", + args: undefined, }, document: { - en: 'Document', - args: undefined, + en: "Document", + args: undefined, }, donate: { - en: 'Donate', - args: undefined, + en: "Donate", + args: undefined, }, done: { - en: 'Done', - args: undefined, + en: "Done", + args: undefined, }, download: { - en: 'Download', - args: undefined, + en: "Download", + args: undefined, }, downloading: { - en: 'Downloading...', - args: undefined, + en: "Downloading...", + args: undefined, }, draft: { - en: 'Draft', - args: undefined, + en: "Draft", + args: undefined, }, edit: { - en: 'Edit', - args: undefined, + en: "Edit", + args: undefined, }, emojiAndSymbols: { - en: 'Emoji and Symbols', - args: undefined, + en: "Emoji and Symbols", + args: undefined, }, emojiCategoryActivities: { - en: 'Activities', - args: undefined, + en: "Activities", + args: undefined, }, emojiCategoryAnimals: { - en: 'Animals and Nature', - args: undefined, + en: "Animals and Nature", + args: undefined, }, emojiCategoryFlags: { - en: 'Flags', - args: undefined, + en: "Flags", + args: undefined, }, emojiCategoryFood: { - en: 'Food and Drink', - args: undefined, + en: "Food and Drink", + args: undefined, }, emojiCategoryObjects: { - en: 'Objects', - args: undefined, + en: "Objects", + args: undefined, }, emojiCategoryRecentlyUsed: { - en: 'Recently Used', - args: undefined, + en: "Recently Used", + args: undefined, }, emojiCategorySmileys: { - en: 'Smileys and People', - args: undefined, + en: "Smileys and People", + args: undefined, }, emojiCategorySymbols: { - en: 'Symbols', - args: undefined, + en: "Symbols", + args: undefined, }, emojiCategoryTravel: { - en: 'Travel and Places', - args: undefined, + en: "Travel and Places", + args: undefined, }, emojiReactsClearAll: { - en: 'Are you sure you want to clear all {emoji}?', - args: { emoji: 'string' }, + en: "Are you sure you want to clear all {emoji}?", + args: {emoji: "string"} }, emojiReactsCoolDown: { - en: "Slow down! You've sent too many emoji reacts. Try again soon", - args: undefined, + en: "Slow down! You've sent too many emoji reacts. Try again soon", + args: undefined, }, emojiReactsHoverNameDesktop: { - en: '{name} reacted with {emoji_name}', - args: { name: 'string', emoji_name: 'string' }, + en: "{name} reacted with {emoji_name}", + args: {name: "string", emoji_name: "string"} }, emojiReactsHoverNameTwoDesktop: { - en: '{name} and {other_name} reacted with {emoji_name}', - args: { name: 'string', other_name: 'string', emoji_name: 'string' }, + en: "{name} and {other_name} reacted with {emoji_name}", + args: {name: "string", other_name: "string", emoji_name: "string"} }, emojiReactsHoverTwoNameMultipleDesktop: { - en: '{name} and {count} others reacted with {emoji_name}', - args: { name: 'string', count: 'number', emoji_name: 'string' }, + en: "{name} and {count} others reacted with {emoji_name}", + args: {name: "string", count: "number", emoji_name: "string"} }, emojiReactsHoverYouNameDesktop: { - en: 'You reacted with {emoji_name}', - args: { emoji_name: 'string' }, + en: "You reacted with {emoji_name}", + args: {emoji_name: "string"} }, emojiReactsHoverYouNameMultipleDesktop: { - en: 'You and {count} others reacted with {emoji_name}', - args: { count: 'number', emoji_name: 'string' }, + en: "You and {count} others reacted with {emoji_name}", + args: {count: "number", emoji_name: "string"} }, emojiReactsHoverYouNameTwoDesktop: { - en: 'You and {name} reacted with {emoji_name}', - args: { name: 'string', emoji_name: 'string' }, + en: "You and {name} reacted with {emoji_name}", + args: {name: "string", emoji_name: "string"} }, emojiReactsNotification: { - en: 'Reacted to your message {emoji}', - args: { emoji: 'string' }, + en: "Reacted to your message {emoji}", + args: {emoji: "string"} }, enable: { - en: 'Enable', - args: undefined, + en: "Enable", + args: undefined, }, errorConnection: { - en: 'Please check your internet connection and try again.', - args: undefined, + en: "Please check your internet connection and try again.", + args: undefined, }, errorCopyAndQuit: { - en: 'Copy Error and Quit', - args: undefined, + en: "Copy Error and Quit", + args: undefined, }, errorDatabase: { - en: 'Database Error', - args: undefined, + en: "Database Error", + args: undefined, }, errorGeneric: { - en: 'Something went wrong. Please try again later.', - args: undefined, + en: "Something went wrong. Please try again later.", + args: undefined, }, errorUnknown: { - en: 'An unknown error occurred.', - args: undefined, + en: "An unknown error occurred.", + args: undefined, }, failedToDownload: { - en: 'Failed to download', - args: undefined, + en: "Failed to download", + args: undefined, }, failures: { - en: 'Failures', - args: undefined, + en: "Failures", + args: undefined, }, file: { - en: 'File', - args: undefined, + en: "File", + args: undefined, }, files: { - en: 'Files', - args: undefined, + en: "Files", + args: undefined, }, followSystemSettings: { - en: 'Follow system settings', - args: undefined, + en: "Follow system settings", + args: undefined, }, forever: { - en: 'Forever', - args: undefined, + en: "Forever", + args: undefined, }, from: { - en: 'From:', - args: undefined, + en: "From:", + args: undefined, }, fullScreenToggle: { - en: 'Toggle Full Screen', - args: undefined, + en: "Toggle Full Screen", + args: undefined, }, gif: { - en: 'GIF', - args: undefined, + en: "GIF", + args: undefined, }, giphyWarning: { - en: 'Giphy', - args: undefined, + en: "Giphy", + args: undefined, }, giphyWarningDescription: { - en: 'Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.', - args: undefined, + en: "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.", + args: undefined, }, groupAddMemberMaximum: { - en: 'Groups have a maximum of 100 members', - args: undefined, + en: "Groups have a maximum of 100 members", + args: undefined, }, groupCreate: { - en: 'Create Group', - args: undefined, + en: "Create Group", + args: undefined, }, groupCreateErrorNoMembers: { - en: 'Please pick at least one other group member.', - args: undefined, + en: "Please pick at least one other group member.", + args: undefined, }, groupDelete: { - en: 'Delete Group', - args: undefined, + en: "Delete Group", + args: undefined, }, groupDeleteDescription: { - en: 'Are you sure you want to delete {group_name}?

This will remove all members and delete all group content.', - args: { group_name: 'string' }, + en: "Are you sure you want to delete {group_name}?

This will remove all members and delete all group content.", + args: {group_name: "string"} }, groupDeleteDescriptionMember: { - en: 'Are you sure you want to delete {group_name}?', - args: { group_name: 'string' }, + en: "Are you sure you want to delete {group_name}?", + args: {group_name: "string"} }, groupDeletedMemberDescription: { - en: '{group_name} has been deleted by a group admin. You will not be able to send any more messages.', - args: { group_name: 'string' }, + en: "{group_name} has been deleted by a group admin. You will not be able to send any more messages.", + args: {group_name: "string"} }, groupDescriptionEnter: { - en: 'Enter a group description', - args: undefined, + en: "Enter a group description", + args: undefined, }, groupDisplayPictureUpdated: { - en: 'Group display picture updated.', - args: undefined, + en: "Group display picture updated.", + args: undefined, }, groupEdit: { - en: 'Edit Group', - args: undefined, + en: "Edit Group", + args: undefined, }, groupError: { - en: 'Group Error', - args: undefined, + en: "Group Error", + args: undefined, }, groupErrorCreate: { - en: 'Failed to create group. Please check your internet connection and try again.', - args: undefined, + en: "Failed to create group. Please check your internet connection and try again.", + args: undefined, }, groupErrorJoin: { - en: 'Failed to join {group_name}', - args: { group_name: 'string' }, + en: "Failed to join {group_name}", + args: {group_name: "string"} }, groupInformationSet: { - en: 'Set Group Information', - args: undefined, + en: "Set Group Information", + args: undefined, }, groupInviteDelete: { - en: 'Are you sure you want to delete this group invite?', - args: undefined, + en: "Are you sure you want to delete this group invite?", + args: undefined, }, groupInviteFailed: { - en: 'Invite failed', - args: undefined, + en: "Invite failed", + args: undefined, }, groupInviteFailedMultiple: { - en: 'Failed to invite {name} and {count} others to {group_name}', - args: { name: 'string', count: 'number', group_name: 'string' }, + en: "Failed to invite {name} and {count} others to {group_name}", + args: {name: "string", count: "number", group_name: "string"} }, groupInviteFailedTwo: { - en: 'Failed to invite {name} and {other_name} to {group_name}', - args: { name: 'string', other_name: 'string', group_name: 'string' }, + en: "Failed to invite {name} and {other_name} to {group_name}", + args: {name: "string", other_name: "string", group_name: "string"} }, groupInviteFailedUser: { - en: 'Failed to invite {name} to {group_name}', - args: { name: 'string', group_name: 'string' }, + en: "Failed to invite {name} to {group_name}", + args: {name: "string", group_name: "string"} }, groupInviteNotSent: { - en: 'Invite not sent', - args: undefined, + en: "Invite not sent", + args: undefined, }, groupInviteReinvite: { - en: '{name} invited you to rejoin {group_name}, where you are an Admin.', - args: { name: 'string', group_name: 'string' }, + en: "{name} invited you to rejoin {group_name}, where you are an Admin.", + args: {name: "string", group_name: "string"} }, groupInviteReinviteYou: { - en: 'You were invited to rejoin {group_name}, where you are an Admin.', - args: { group_name: 'string' }, + en: "You were invited to rejoin {group_name}, where you are an Admin.", + args: {group_name: "string"} }, groupInviteSent: { - en: 'Invite sent', - args: undefined, + en: "Invite sent", + args: undefined, }, groupInviteStatusUnknown: { - en: 'Invite status unknown', - args: undefined, + en: "Invite status unknown", + args: undefined, }, groupInviteSuccessful: { - en: 'Group invite successful', - args: undefined, + en: "Group invite successful", + args: undefined, }, groupInviteVersion: { - en: 'Users must have the latest release to receive invitations', - args: undefined, + en: "Users must have the latest release to receive invitations", + args: undefined, }, groupInviteYou: { - en: 'You were invited to join the group.', - args: undefined, + en: "You were invited to join the group.", + args: undefined, }, groupInviteYouAndMoreNew: { - en: 'You and {count} others were invited to join the group.', - args: { count: 'number' }, + en: "You and {count} others were invited to join the group.", + args: {count: "number"} }, groupInviteYouAndOtherNew: { - en: 'You and {other_name} were invited to join the group.', - args: { other_name: 'string' }, + en: "You and {other_name} were invited to join the group.", + args: {other_name: "string"} }, groupInviteYouHistory: { - en: 'You were invited to join the group. Chat history was shared.', - args: undefined, + en: "You were invited to join the group. Chat history was shared.", + args: undefined, }, groupLeave: { - en: 'Leave Group', - args: undefined, + en: "Leave Group", + args: undefined, }, groupLeaveDescription: { - en: 'Are you sure you want to leave {group_name}?', - args: { group_name: 'string' }, + en: "Are you sure you want to leave {group_name}?", + args: {group_name: "string"} }, groupLeaveDescriptionAdmin: { - en: 'Are you sure you want to leave {group_name}?

This will remove all members and delete all group content.', - args: { group_name: 'string' }, + en: "Are you sure you want to leave {group_name}?

This will remove all members and delete all group content.", + args: {group_name: "string"} }, groupLeaveErrorFailed: { - en: 'Failed to leave {group_name}', - args: { group_name: 'string' }, + en: "Failed to leave {group_name}", + args: {group_name: "string"} }, groupMemberLeft: { - en: '{name} left the group.', - args: { name: 'string' }, + en: "{name} left the group.", + args: {name: "string"} }, groupMemberLeftMultiple: { - en: '{name} and {count} others left the group.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others left the group.", + args: {name: "string", count: "number"} }, groupMemberLeftTwo: { - en: '{name} and {other_name} left the group.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} left the group.", + args: {name: "string", other_name: "string"} }, groupMemberNew: { - en: '{name} was invited to join the group.', - args: { name: 'string' }, + en: "{name} was invited to join the group.", + args: {name: "string"} }, groupMemberNewHistory: { - en: '{name} was invited to join the group. Chat history was shared.', - args: { name: 'string' }, + en: "{name} was invited to join the group. Chat history was shared.", + args: {name: "string"} }, groupMemberNewHistoryMultiple: { - en: '{name} and {count} others were invited to join the group. Chat history was shared.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others were invited to join the group. Chat history was shared.", + args: {name: "string", count: "number"} }, groupMemberNewHistoryTwo: { - en: '{name} and {other_name} were invited to join the group. Chat history was shared.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} were invited to join the group. Chat history was shared.", + args: {name: "string", other_name: "string"} }, groupMemberNewMultiple: { - en: '{name} and {count} others were invited to join the group.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others were invited to join the group.", + args: {name: "string", count: "number"} }, groupMemberNewTwo: { - en: '{name} and {other_name} were invited to join the group.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} were invited to join the group.", + args: {name: "string", other_name: "string"} }, groupMemberNewYouHistoryMultiple: { - en: 'You and {count} others were invited to join the group. Chat history was shared.', - args: { count: 'number' }, + en: "You and {count} others were invited to join the group. Chat history was shared.", + args: {count: "number"} }, groupMemberNewYouHistoryTwo: { - en: 'You and {other_name} were invited to join the group. Chat history was shared.', - args: { other_name: 'string' }, + en: "You and {other_name} were invited to join the group. Chat history was shared.", + args: {other_name: "string"} }, groupMemberYouLeft: { - en: 'You left the group.', - args: undefined, + en: "You left the group.", + args: undefined, }, groupMembers: { - en: 'Group Members', - args: undefined, + en: "Group Members", + args: undefined, }, groupMembersNone: { - en: 'There are no other members in this group.', - args: undefined, + en: "There are no other members in this group.", + args: undefined, }, groupName: { - en: 'Group Name', - args: undefined, + en: "Group Name", + args: undefined, }, groupNameEnter: { - en: 'Enter a group name', - args: undefined, + en: "Enter a group name", + args: undefined, }, groupNameEnterPlease: { - en: 'Please enter a group name.', - args: undefined, + en: "Please enter a group name.", + args: undefined, }, groupNameEnterShorter: { - en: 'Please enter a shorter group name.', - args: undefined, + en: "Please enter a shorter group name", + args: undefined, }, groupNameNew: { - en: 'Group name is now {group_name}.', - args: { group_name: 'string' }, + en: "Group name is now {group_name}.", + args: {group_name: "string"} }, groupNameUpdated: { - en: 'Group name updated.', - args: undefined, + en: "Group name updated.", + args: undefined, }, groupNameVisible: { - en: 'Group name is visible to all group members.', - args: undefined, + en: "Group name is visible to all group members.", + args: undefined, }, groupNoMessages: { - en: 'You have no messages from {group_name}. Send a message to start the conversation!', - args: { group_name: 'string' }, + en: "You have no messages from {group_name}. Send a message to start the conversation!", + args: {group_name: "string"} }, groupNotUpdatedWarning: { - en: 'This group has not been updated in over 30 days. You may experience issues sending messages or viewing group information.', - args: undefined, + en: "This group has not been updated in over 30 days. You may experience issues sending messages or viewing group information.", + args: undefined, }, groupOnlyAdmin: { - en: 'You are the only admin in {group_name}.

Group members and settings cannot be changed without an admin.', - args: { group_name: 'string' }, + en: "You are the only admin in {group_name}.

Group members and settings cannot be changed without an admin.", + args: {group_name: "string"} }, groupPendingRemoval: { - en: 'Pending removal', - args: undefined, + en: "Pending removal", + args: undefined, }, groupPromotedYou: { - en: 'You were promoted to Admin.', - args: undefined, + en: "You were promoted to Admin.", + args: undefined, }, groupPromotedYouMultiple: { - en: 'You and {count} others were promoted to Admin.', - args: { count: 'number' }, + en: "You and {count} others were promoted to Admin.", + args: {count: "number"} }, groupPromotedYouTwo: { - en: 'You and {other_name} were promoted to Admin.', - args: { other_name: 'string' }, + en: "You and {other_name} were promoted to Admin.", + args: {other_name: "string"} }, groupRemoveDescription: { - en: 'Would you like to remove {name} from {group_name}?', - args: { name: 'string', group_name: 'string' }, + en: "Would you like to remove {name} from {group_name}?", + args: {name: "string", group_name: "string"} }, groupRemoveDescriptionMultiple: { - en: 'Would you like to remove {name} and {count} others from {group_name}?', - args: { name: 'string', count: 'number', group_name: 'string' }, + en: "Would you like to remove {name} and {count} others from {group_name}?", + args: {name: "string", count: "number", group_name: "string"} }, groupRemoveDescriptionTwo: { - en: 'Would you like to remove {name} and {other_name} from {group_name}?', - args: { name: 'string', other_name: 'string', group_name: 'string' }, + en: "Would you like to remove {name} and {other_name} from {group_name}?", + args: {name: "string", other_name: "string", group_name: "string"} }, groupRemoved: { - en: '{name} was removed from the group.', - args: { name: 'string' }, + en: "{name} was removed from the group.", + args: {name: "string"} }, groupRemovedMultiple: { - en: '{name} and {count} others were removed from the group.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others were removed from the group.", + args: {name: "string", count: "number"} }, groupRemovedTwo: { - en: '{name} and {other_name} were removed from the group.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} were removed from the group.", + args: {name: "string", other_name: "string"} }, groupRemovedYou: { - en: 'You were removed from {group_name}.', - args: { group_name: 'string' }, + en: "You were removed from {group_name}.", + args: {group_name: "string"} }, groupRemovedYouGeneral: { - en: 'You were removed from the group.', - args: undefined, + en: "You were removed from the group.", + args: undefined, }, groupRemovedYouMultiple: { - en: 'You and {count} others were removed from the group.', - args: { count: 'number' }, + en: "You and {count} others were removed from the group.", + args: {count: "number"} }, groupRemovedYouTwo: { - en: 'You and {other_name} were removed from the group.', - args: { other_name: 'string' }, + en: "You and {other_name} were removed from the group.", + args: {other_name: "string"} }, groupSetDisplayPicture: { - en: 'Set Group Display Picture', - args: undefined, + en: "Set Group Display Picture", + args: undefined, }, groupUnknown: { - en: 'Unknown Group', - args: undefined, + en: "Unknown Group", + args: undefined, }, groupUpdated: { - en: 'Group updated', - args: undefined, + en: "Group updated", + args: undefined, }, handlingConnectionCandidates: { - en: 'Handling Connection Candidates', - args: undefined, + en: "Handling Connection Candidates", + args: undefined, }, helpFAQ: { - en: 'FAQ', - args: undefined, + en: "FAQ", + args: undefined, }, helpHelpUsTranslateSession: { - en: 'Help us translate Session', - args: undefined, + en: "Help us translate Session", + args: undefined, }, helpReportABug: { - en: 'Report a bug', - args: undefined, + en: "Report a bug", + args: undefined, }, helpReportABugDescription: { - en: "Share some details to help us resolve your issue. Export your logs, then upload the file through Session's Help Desk.", - args: undefined, + en: "Share some details to help us resolve your issue. Export your logs, then upload the file through Session's Help Desk.", + args: undefined, }, helpReportABugExportLogs: { - en: 'Export Logs', - args: undefined, + en: "Export Logs", + args: undefined, }, helpReportABugExportLogsDescription: { - en: "Export your logs, then upload the file through Session's Help Desk.", - args: undefined, + en: "Export your logs, then upload the file through Session's Help Desk.", + args: undefined, }, helpReportABugExportLogsSaveToDesktop: { - en: 'Save to desktop', - args: undefined, + en: "Save to desktop", + args: undefined, }, helpReportABugExportLogsSaveToDesktopDescription: { - en: 'Save this file to your desktop, then share it with Session developers.', - args: undefined, + en: "Save this file to your desktop, then share it with Session developers.", + args: undefined, }, helpSupport: { - en: 'Support', - args: undefined, + en: "Support", + args: undefined, }, helpWedLoveYourFeedback: { - en: "We'd love your feedback", - args: undefined, + en: "We'd love your feedback", + args: undefined, }, hide: { - en: 'Hide', - args: undefined, + en: "Hide", + args: undefined, }, hideMenuBarDescription: { - en: 'Toggle system menu bar visibility', - args: undefined, + en: "Toggle system menu bar visibility", + args: undefined, }, hideNoteToSelfDescription: { - en: 'Are you sure you want to hide Note to Self from your conversation list?', - args: undefined, + en: "Are you sure you want to hide Note to Self from your conversation list?", + args: undefined, }, hideOthers: { - en: 'Hide Others', - args: undefined, + en: "Hide Others", + args: undefined, }, image: { - en: 'Image', - args: undefined, + en: "Image", + args: undefined, }, images: { - en: 'images', - args: undefined, + en: "images", + args: undefined, }, incognitoKeyboard: { - en: 'Incognito Keyboard', - args: undefined, + en: "Incognito Keyboard", + args: undefined, }, incognitoKeyboardDescription: { - en: 'Request incognito mode if available. Depending on the keyboard you are using, your keyboard may ignore this request.', - args: undefined, + en: "Request incognito mode if available. Depending on the keyboard you are using, your keyboard may ignore this request.", + args: undefined, }, info: { - en: 'Info', - args: undefined, + en: "Info", + args: undefined, }, invalidShortcut: { - en: 'Invalid shortcut', - args: undefined, + en: "Invalid shortcut", + args: undefined, }, join: { - en: 'Join', - args: undefined, + en: "Join", + args: undefined, }, later: { - en: 'Later', - args: undefined, + en: "Later", + args: undefined, }, learnMore: { - en: 'Learn More', - args: undefined, + en: "Learn More", + args: undefined, }, leave: { - en: 'Leave', - args: undefined, + en: "Leave", + args: undefined, }, leaving: { - en: 'Leaving...', - args: undefined, + en: "Leaving...", + args: undefined, }, legacyGroupAfterDeprecationAdmin: { - en: 'This group is now read-only. Recreate this group to keep chatting.', - args: undefined, + en: "This group is now read-only. Recreate this group to keep chatting.", + args: undefined, }, legacyGroupAfterDeprecationMember: { - en: 'This group is now read-only. Ask the group admin to recreate this group to keep chatting.', - args: undefined, + en: "This group is now read-only. Ask the group admin to recreate this group to keep chatting.", + args: undefined, }, legacyGroupBeforeDeprecationAdmin: { - en: 'Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only at {date}.', - args: { date: 'string' }, + en: "Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only at {date}.", + args: {date: "string"} }, legacyGroupBeforeDeprecationMember: { - en: 'Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only at {date}.', - args: { date: 'string' }, + en: "Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only at {date}.", + args: {date: "string"} }, legacyGroupChatHistory: { - en: 'Chat history will not be transferred to the new group. You can still view all chat history in your old group.', - args: undefined, + en: "Chat history will not be transferred to the new group. You can still view all chat history in your old group.", + args: undefined, }, legacyGroupMemberNew: { - en: '{name} joined the group.', - args: { name: 'string' }, + en: "{name} joined the group.", + args: {name: "string"} }, legacyGroupMemberNewMultiple: { - en: '{name} and {count} others joined the group.', - args: { name: 'string', count: 'number' }, + en: "{name} and {count} others joined the group.", + args: {name: "string", count: "number"} }, legacyGroupMemberNewYouMultiple: { - en: 'You and {count} others joined the group.', - args: { count: 'number' }, + en: "You and {count} others joined the group.", + args: {count: "number"} }, legacyGroupMemberNewYouOther: { - en: 'You and {other_name} joined the group.', - args: { other_name: 'string' }, + en: "You and {other_name} joined the group.", + args: {other_name: "string"} }, legacyGroupMemberTwoNew: { - en: '{name} and {other_name} joined the group.', - args: { name: 'string', other_name: 'string' }, + en: "{name} and {other_name} joined the group.", + args: {name: "string", other_name: "string"} }, legacyGroupMemberYouNew: { - en: 'You joined the group.', - args: undefined, + en: "You joined the group.", + args: undefined, }, linkPreviews: { - en: 'Link Previews', - args: undefined, + en: "Link Previews", + args: undefined, }, linkPreviewsDescription: { - en: 'Show link previews for supported URLs.', - args: undefined, + en: "Show link previews for supported URLs.", + args: undefined, }, linkPreviewsEnable: { - en: 'Enable Link Previews', - args: undefined, + en: "Enable Link Previews", + args: undefined, }, linkPreviewsErrorLoad: { - en: 'Unable to load link preview', - args: undefined, + en: "Unable to load link preview", + args: undefined, }, linkPreviewsErrorUnsecure: { - en: 'Preview not loaded for unsecure link', - args: undefined, + en: "Preview not loaded for unsecure link", + args: undefined, }, linkPreviewsFirstDescription: { - en: "Display previews for URLs you send and receive. This can be useful, however Session must contact linked websites to generate previews. You can always turn off link previews in Session's settings.", - args: undefined, + en: "Display previews for URLs you send and receive. This can be useful, however Session must contact linked websites to generate previews. You can always turn off link previews in Session's settings.", + args: undefined, }, linkPreviewsSend: { - en: 'Send Link Previews', - args: undefined, + en: "Send Link Previews", + args: undefined, }, linkPreviewsSendModalDescription: { - en: 'You will not have full metadata protection when sending link previews.', - args: undefined, + en: "You will not have full metadata protection when sending link previews.", + args: undefined, }, linkPreviewsTurnedOff: { - en: 'Link Previews Are Off', - args: undefined, + en: "Link Previews Are Off", + args: undefined, }, 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.", - args: undefined, + en: "Session must contact linked websites to generate previews of links you send and receive.

You can turn them on in Session's settings.", + args: undefined, }, loadAccount: { - en: 'Load Account', - args: undefined, + en: "Load Account", + args: undefined, }, loadAccountProgressMessage: { - en: 'Loading your account', - args: undefined, + en: "Loading your account", + args: undefined, }, loading: { - en: 'Loading...', - args: undefined, + en: "Loading...", + args: undefined, }, lockApp: { - en: 'Lock App', - args: undefined, + en: "Lock App", + args: undefined, }, lockAppDescription: { - en: 'Require fingerprint, PIN, pattern or password to unlock Session.', - args: undefined, + en: "Require fingerprint, PIN, pattern or password to unlock Session.", + args: undefined, }, lockAppDescriptionIos: { - en: 'Require Touch ID, Face ID or your passcode to unlock Session.', - args: undefined, + en: "Require Touch ID, Face ID or your passcode to unlock Session.", + args: undefined, }, lockAppEnablePasscode: { - en: 'You must enable a passcode in your iOS Settings in order to use Screen Lock.', - args: undefined, + en: "You must enable a passcode in your iOS Settings in order to use Screen Lock.", + args: undefined, }, lockAppLocked: { - en: 'Session is locked', - args: undefined, + en: "Session is locked", + args: undefined, }, lockAppQuickResponse: { - en: 'Quick response unavailable when Session is locked!', - args: undefined, + en: "Quick response unavailable when Session is locked!", + args: undefined, }, lockAppStatus: { - en: 'Lock status', - args: undefined, + en: "Lock status", + args: undefined, }, lockAppUnlock: { - en: 'Tap to unlock', - args: undefined, + en: "Tap to unlock", + args: undefined, }, lockAppUnlocked: { - en: 'Session is unlocked', - args: undefined, + en: "Session is unlocked", + args: undefined, }, manageMembers: { - en: 'Manage Members', - args: undefined, + en: "Manage Members", + args: undefined, }, max: { - en: 'Max', - args: undefined, + en: "Max", + args: undefined, }, media: { - en: 'Media', - args: undefined, + en: "Media", + args: undefined, }, membersAddAccountIdOrOns: { - en: 'Add Account ID or ONS', - args: undefined, + en: "Add Account ID or ONS", + args: undefined, }, membersInvite: { - en: 'Invite Contacts', - args: undefined, + en: "Invite Contacts", + args: undefined, }, membersInviteShareDescription: { - en: 'Would you like to share group message history with {name}?', - args: { name: 'string' }, + en: "Would you like to share group message history with {name}?", + args: {name: "string"} }, membersInviteShareDescriptionMultiple: { - en: 'Would you like to share group message history with {name} and {count} others?', - args: { name: 'string', count: 'number' }, + en: "Would you like to share group message history with {name} and {count} others?", + args: {name: "string", count: "number"} }, membersInviteShareDescriptionTwo: { - en: 'Would you like to share group message history with {name} and {other_name}?', - args: { name: 'string', other_name: 'string' }, + en: "Would you like to share group message history with {name} and {other_name}?", + args: {name: "string", other_name: "string"} }, membersInviteShareMessageHistory: { - en: 'Share message history', - args: undefined, + en: "Share message history", + args: undefined, }, membersInviteShareNewMessagesOnly: { - en: 'Share new messages only', - args: undefined, + en: "Share new messages only", + args: undefined, }, membersInviteTitle: { - en: 'Invite', - args: undefined, + en: "Invite", + args: undefined, }, message: { - en: 'Message', - args: undefined, + en: "Message", + args: undefined, + }, + messageBubbleReadMore: { + en: "Read more", + args: undefined, }, messageEmpty: { - en: 'This message is empty.', - args: undefined, + en: "This message is empty.", + args: undefined, }, messageErrorDelivery: { - en: 'Message delivery failed', - args: undefined, + en: "Message delivery failed", + args: undefined, }, messageErrorLimit: { - en: 'Message limit reached', - args: undefined, + en: "Message limit reached", + args: undefined, }, messageErrorOld: { - en: 'Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.', - args: undefined, + en: "Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.", + args: undefined, }, messageErrorOriginal: { - en: 'Original message not found', - args: undefined, + en: "Original message not found", + args: undefined, }, messageInfo: { - en: 'Message Info', - args: undefined, + en: "Message Info", + args: undefined, }, messageMarkRead: { - en: 'Mark read', - args: undefined, + en: "Mark read", + args: undefined, }, messageMarkUnread: { - en: 'Mark unread', - args: undefined, + en: "Mark unread", + args: undefined, }, messageNewDescriptionDesktop: { - en: "Start a new conversation by entering your friend's Account ID or ONS.", - args: undefined, + en: "Start a new conversation by entering your friend's Account ID or ONS.", + args: undefined, }, messageNewDescriptionMobile: { - en: "Start a new conversation by entering your friend's Account ID, ONS or scanning their QR code.", - args: undefined, + en: "Start a new conversation by entering your friend's Account ID, ONS or scanning their QR code.", + args: undefined, }, messageReplyingTo: { - en: 'Replying to', - args: undefined, + en: "Replying to", + args: undefined, }, messageRequestDisabledToastAttachments: { - en: 'You cannot send attachments until your Message Request is accepted', - args: undefined, + en: "You cannot send attachments until your Message Request is accepted", + args: undefined, }, messageRequestDisabledToastVoiceMessages: { - en: 'You cannot send voice messages until your Message Request is accepted', - args: undefined, + en: "You cannot send voice messages until your Message Request is accepted", + args: undefined, }, messageRequestGroupInvite: { - en: '{name} invited you to join {group_name}.', - args: { name: 'string', group_name: 'string' }, + en: "{name} invited you to join {group_name}.", + args: {name: "string", group_name: "string"} }, messageRequestGroupInviteDescription: { - en: 'Sending a message to this group will automatically accept the group invite.', - args: undefined, + en: "Sending a message to this group will automatically accept the group invite.", + args: undefined, }, messageRequestPending: { - en: 'Your message request is currently pending.', - args: undefined, + en: "Your message request is currently pending.", + args: undefined, }, messageRequestPendingDescription: { - en: 'You will be able to send voice messages and attachments once the recipient has approved this message request.', - args: undefined, + en: "You will be able to send voice messages and attachments once the recipient has approved this message request.", + args: undefined, }, messageRequestYouHaveAccepted: { - en: 'You have accepted the message request from {name}.', - args: { name: 'string' }, + en: "You have accepted the message request from {name}.", + args: {name: "string"} }, messageRequestsAcceptDescription: { - en: 'Sending a message to this user will automatically accept their message request and reveal your Account ID.', - args: undefined, + en: "Sending a message to this user will automatically accept their message request and reveal your Account ID.", + args: undefined, }, messageRequestsAccepted: { - en: 'Your message request has been accepted.', - args: undefined, + en: "Your message request has been accepted.", + args: undefined, }, messageRequestsClearAllExplanation: { - en: 'Are you sure you want to clear all message requests and group invites?', - args: undefined, + en: "Are you sure you want to clear all message requests and group invites?", + args: undefined, }, messageRequestsCommunities: { - en: 'Community Message Requests', - args: undefined, + en: "Community Message Requests", + args: undefined, }, messageRequestsCommunitiesDescription: { - en: 'Allow message requests from Community conversations.', - args: undefined, + en: "Allow message requests from Community conversations.", + args: undefined, }, messageRequestsContactDelete: { - en: 'Are you sure you want to delete this message request and the associated contact?', - args: undefined, + en: "Are you sure you want to delete this message request and the associated contact?", + args: undefined, }, messageRequestsDelete: { - en: 'Are you sure you want to delete this message request?', - args: undefined, + en: "Are you sure you want to delete this message request?", + args: undefined, }, messageRequestsNew: { - en: 'You have a new message request', - args: undefined, + en: "You have a new message request", + args: undefined, }, messageRequestsNonePending: { - en: 'No pending message requests', - args: undefined, + en: "No pending message requests", + args: undefined, }, messageRequestsTurnedOff: { - en: '{name} has message requests from Community conversations turned off, so you cannot send them a message.', - args: { name: 'string' }, + en: "{name} has message requests from Community conversations turned off, so you cannot send them a message.", + args: {name: "string"} }, messageSelect: { - en: 'Select Message', - args: undefined, + en: "Select Message", + args: undefined, }, messageSnippetGroup: { - en: '{author}: {message_snippet}', - args: { author: 'string', message_snippet: 'string' }, + en: "{author}: {message_snippet}", + args: {author: "string", message_snippet: "string"} }, messageStatusFailedToSend: { - en: 'Failed to send', - args: undefined, + en: "Failed to send", + args: undefined, }, messageStatusFailedToSync: { - en: 'Failed to sync', - args: undefined, + en: "Failed to sync", + args: undefined, }, messageStatusSyncing: { - en: 'Syncing', - args: undefined, + en: "Syncing", + args: undefined, }, messageUnread: { - en: 'Unread messages', - args: undefined, + en: "Unread messages", + args: undefined, }, messageVoice: { - en: 'Voice Message', - args: undefined, + en: "Voice Message", + args: undefined, }, messageVoiceErrorShort: { - en: 'Hold to record a voice message', - args: undefined, + en: "Hold to record a voice message", + args: undefined, }, messageVoiceSlideToCancel: { - en: 'Slide to Cancel', - args: undefined, + en: "Slide to Cancel", + args: undefined, }, messageVoiceSnippet: { - en: '{emoji} Voice Message', - args: { emoji: 'string' }, + en: "{emoji} Voice Message", + args: {emoji: "string"} }, messageVoiceSnippetGroup: { - en: '{author}: {emoji} Voice Message', - args: { author: 'string', emoji: 'string' }, + en: "{author}: {emoji} Voice Message", + args: {author: "string", emoji: "string"} }, messages: { - en: 'Messages', - args: undefined, + en: "Messages", + args: undefined, }, minimize: { - en: 'Minimize', - args: undefined, + en: "Minimize", + args: undefined, + }, + modalMessageCharacterDisplayTitle: { + en: "Message Length", + args: undefined, + }, + modalMessageCharacterTooLongDescription: { + en: "You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less.", + args: {limit: "string"} + }, + modalMessageCharacterTooLongTitle: { + en: "Message Too Long", + args: undefined, }, modalMessageTooLongDescription: { - en: 'Please shorten your message to {count} characters or less.', - args: { count: 'number' }, + en: "Please shorten your message to {limit} characters or less.", + args: {limit: "string"} }, modalMessageTooLongTitle: { - en: 'Your message is too long', - args: undefined, + en: "Message Too Long", + args: undefined, }, next: { - en: 'Next', - args: undefined, + en: "Next", + args: undefined, }, nicknameDescription: { - en: 'Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations.', - args: { name: 'string' }, + en: "Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations.", + args: {name: "string"} }, nicknameEnter: { - en: 'Enter nickname', - args: undefined, + en: "Enter nickname", + args: undefined, }, nicknameErrorShorter: { - en: 'Please enter a shorter nickname', - args: undefined, + en: "Please enter a shorter nickname", + args: undefined, }, nicknameRemove: { - en: 'Remove Nickname', - args: undefined, + en: "Remove Nickname", + args: undefined, }, nicknameSet: { - en: 'Set Nickname', - args: undefined, + en: "Set Nickname", + args: undefined, }, no: { - en: 'No', - args: undefined, + en: "No", + args: undefined, }, noSuggestions: { - en: 'No Suggestions', - args: undefined, + en: "No Suggestions", + args: undefined, }, none: { - en: 'None', - args: undefined, + en: "None", + args: undefined, }, notNow: { - en: 'Not now', - args: undefined, + en: "Not now", + args: undefined, }, noteToSelf: { - en: 'Note to Self', - args: undefined, + en: "Note to Self", + args: undefined, }, noteToSelfEmpty: { - en: 'You have no messages in Note to Self.', - args: undefined, + en: "You have no messages in Note to Self.", + args: undefined, }, noteToSelfHide: { - en: 'Hide Note to Self', - args: undefined, + en: "Hide Note to Self", + args: undefined, }, noteToSelfHideDescription: { - en: 'Are you sure you want to hide Note to Self?', - args: undefined, + en: "Are you sure you want to hide Note to Self?", + args: undefined, }, notificationsAllMessages: { - en: 'All Messages', - args: undefined, + en: "All Messages", + args: undefined, }, notificationsContent: { - en: 'Notification Content', - args: undefined, + en: "Notification Content", + args: undefined, }, notificationsContentDescription: { - en: 'The information shown in notifications.', - args: undefined, + en: "The information shown in notifications.", + args: undefined, }, notificationsContentShowNameAndContent: { - en: 'Name and Content', - args: undefined, + en: "Name and Content", + args: undefined, }, notificationsContentShowNameOnly: { - en: 'Name Only', - args: undefined, + en: "Name Only", + args: undefined, }, notificationsContentShowNoNameOrContent: { - en: 'No Name or Content', - args: undefined, + en: "No Name or Content", + args: undefined, }, notificationsFastMode: { - en: 'Fast Mode', - args: undefined, + en: "Fast Mode", + args: undefined, }, notificationsFastModeDescription: { - en: "You'll be notified of new messages reliably and immediately using Google's notification Servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Google's notification Servers.", + args: undefined, }, notificationsFastModeDescriptionHuawei: { - en: "You'll be notified of new messages reliably and immediately using Huawei’s notification servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Huawei’s notification servers.", + args: undefined, }, notificationsFastModeDescriptionIos: { - en: "You'll be notified of new messages reliably and immediately using Apple's notification Servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Apple's notification Servers.", + args: undefined, }, notificationsGoToDevice: { - en: 'Go to device notification settings', - args: undefined, + en: "Go to device notification settings", + args: undefined, }, notificationsHeaderAllMessages: { - en: 'Notifications - All', - args: undefined, + en: "Notifications - All", + args: undefined, }, notificationsHeaderMentionsOnly: { - en: 'Notifications - Mentions Only', - args: undefined, + en: "Notifications - Mentions Only", + args: undefined, }, notificationsHeaderMute: { - en: 'Notifications - Muted', - args: undefined, + en: "Notifications - Muted", + args: undefined, }, notificationsIosGroup: { - en: '{name} to {conversation_name}', - args: { name: 'string', conversation_name: 'string' }, + en: "{name} to {conversation_name}", + args: {name: "string", conversation_name: "string"} }, notificationsIosRestart: { - en: 'You may have received messages while your {device} was restarting.', - args: { device: 'string' }, + en: "You may have received messages while your {device} was restarting.", + args: {device: "string"} }, notificationsLedColor: { - en: 'LED color', - args: undefined, + en: "LED color", + args: undefined, }, notificationsMentionsOnly: { - en: 'Mentions Only', - args: undefined, + en: "Mentions Only", + args: undefined, }, notificationsMessage: { - en: 'Message notifications', - args: undefined, + en: "Message notifications", + args: undefined, }, notificationsMostRecent: { - en: 'Most recent from {name}', - args: { name: 'string' }, + en: "Most recent from {name}", + args: {name: "string"} }, notificationsMute: { - en: 'Mute', - args: undefined, + en: "Mute", + args: undefined, }, notificationsMuteFor: { - en: 'Mute for {time_large}', - args: { time_large: 'string' }, + en: "Mute for {time_large}", + args: {time_large: "string"} }, notificationsMuteUnmute: { - en: 'Unmute', - args: undefined, + en: "Unmute", + args: undefined, }, notificationsMuted: { - en: 'Muted', - args: undefined, + en: "Muted", + args: undefined, }, notificationsMutedFor: { - en: 'Muted for {time_large}', - args: { time_large: 'string' }, + en: "Muted for {time_large}", + args: {time_large: "string"} }, notificationsMutedForTime: { - en: 'Muted until {date_time}', - args: { date_time: 'string' }, + en: "Muted until {date_time}", + args: {date_time: "string"} }, notificationsSlowMode: { - en: 'Slow Mode', - args: undefined, + en: "Slow Mode", + args: undefined, }, notificationsSlowModeDescription: { - en: 'Session will occasionally check for new messages in the background.', - args: undefined, + en: "Session will occasionally check for new messages in the background.", + args: undefined, }, notificationsSound: { - en: 'Sound', - args: undefined, + en: "Sound", + args: undefined, }, notificationsSoundDescription: { - en: 'Sound when App is open', - args: undefined, + en: "Sound when App is open", + args: undefined, }, notificationsSoundDesktop: { - en: 'Audio Notifications', - args: undefined, + en: "Audio Notifications", + args: undefined, }, notificationsStrategy: { - en: 'Notification Strategy', - args: undefined, + en: "Notification Strategy", + args: undefined, }, notificationsStyle: { - en: 'Notification Style', - args: undefined, + en: "Notification Style", + args: undefined, }, notificationsSystem: { - en: '{message_count} new messages in {conversation_count} conversations', - args: { message_count: 'string', conversation_count: 'string' }, + en: "{message_count} new messages in {conversation_count} conversations", + args: {message_count: "string", conversation_count: "string"} }, notificationsVibrate: { - en: 'Vibrate', - args: undefined, + en: "Vibrate", + args: undefined, }, off: { - en: 'Off', - args: undefined, + en: "Off", + args: undefined, }, okay: { - en: 'Okay', - args: undefined, + en: "Okay", + args: undefined, }, on: { - en: 'On', - args: undefined, + en: "On", + args: undefined, }, onboardingAccountCreate: { - en: 'Create account', - args: undefined, + en: "Create account", + args: undefined, }, onboardingAccountCreated: { - en: 'Account Created', - args: undefined, + en: "Account Created", + args: undefined, }, onboardingAccountExists: { - en: 'I have an account', - args: undefined, + en: "I have an account", + args: undefined, }, onboardingBackAccountCreation: { - en: 'You cannot go back further. In order to cancel your account creation, Session needs to quit.', - args: undefined, + en: "You cannot go back further. In order to cancel your account creation, Session needs to quit.", + args: undefined, }, onboardingBackLoadAccount: { - en: 'You cannot go back further. In order to stop loading your account, Session needs to quit.', - args: undefined, + en: "You cannot go back further. In order to stop loading your account, Session needs to quit.", + args: undefined, }, onboardingBubbleCreatingAnAccountIsEasy: { - en: 'Creating an account is instant, free, and anonymous {emoji}', - args: { emoji: 'string' }, + en: "Creating an account is instant, free, and anonymous {emoji}", + args: {emoji: "string"} }, onboardingBubbleNoPhoneNumber: { - en: "You don't even need a phone number to sign up.", - args: undefined, + en: "You don't even need a phone number to sign up.", + args: undefined, }, onboardingBubblePrivacyInYourPocket: { - en: 'Privacy in your pocket.', - args: undefined, + en: "Privacy in your pocket.", + args: undefined, }, onboardingBubbleSessionIsEngineered: { - en: 'Session is engineered to protect your privacy.', - args: undefined, + en: "Session is engineered to protect your privacy.", + args: undefined, }, onboardingBubbleWelcomeToSession: { - en: 'Welcome to Session {emoji}', - args: { emoji: 'string' }, + en: "Welcome to Session {emoji}", + args: {emoji: "string"} }, onboardingHitThePlusButton: { - en: 'Hit the plus button to start a chat, create a group, or join an official community!', - args: undefined, + en: "Hit the plus button to start a chat, create a group, or join an official community!", + args: undefined, }, onboardingMessageNotificationExplanation: { - en: 'There are two ways Session can notify you of new messages.', - args: undefined, + en: "There are two ways Session can notify you of new messages.", + args: undefined, }, onboardingPrivacy: { - en: 'Privacy Policy', - args: undefined, + en: "Privacy Policy", + args: undefined, }, onboardingTos: { - en: 'Terms of Service', - args: undefined, + en: "Terms of Service", + args: undefined, }, onboardingTosPrivacy: { - en: 'By using this service, you agree to our Terms of Service and Privacy Policy', - args: undefined, + en: "By using this service, you agree to our Terms of Service and Privacy Policy", + args: undefined, }, onionRoutingPath: { - en: 'Path', - args: undefined, + en: "Path", + args: undefined, }, onionRoutingPathDescription: { - en: "Session hides your IP by routing your messages through multiple service nodes in Session's decentralized network. This is your current path:", - args: undefined, + en: "Session hides your IP by routing your messages through multiple service nodes in Session's decentralized network. This is your current path:", + args: undefined, }, onionRoutingPathDestination: { - en: 'Destination', - args: undefined, + en: "Destination", + args: undefined, }, onionRoutingPathEntryNode: { - en: 'Entry Node', - args: undefined, + en: "Entry Node", + args: undefined, }, onionRoutingPathServiceNode: { - en: 'Service Node', - args: undefined, + en: "Service Node", + args: undefined, }, onionRoutingPathUnknownCountry: { - en: 'Unknown Country', - args: undefined, + en: "Unknown Country", + args: undefined, }, onsErrorNotRecognized: { - en: "We couldn't recognize this ONS. Please check it and try again.", - args: undefined, + en: "We couldn't recognize this ONS. Please check it and try again.", + args: undefined, }, onsErrorUnableToSearch: { - en: 'We were unable to search for this ONS. Please try again later.', - args: undefined, + en: "We were unable to search for this ONS. Please try again later.", + args: undefined, }, open: { - en: 'Open', - args: undefined, + en: "Open", + args: undefined, }, other: { - en: 'Other', - args: undefined, + en: "Other", + args: undefined, }, passwordChange: { - en: 'Change Password', - args: undefined, + en: "Change Password", + args: undefined, }, passwordChangeDescription: { - en: 'Change the password required to unlock Session.', - args: undefined, + en: "Change the password required to unlock Session.", + args: undefined, }, passwordChangedDescription: { - en: 'Your password has been changed. Please keep it safe.', - args: undefined, + en: "Your password has been changed. Please keep it safe.", + args: undefined, }, passwordConfirm: { - en: 'Confirm password', - args: undefined, + en: "Confirm password", + args: undefined, }, passwordCreate: { - en: 'Create your password', - args: undefined, + en: "Create your password", + args: undefined, }, passwordCurrentIncorrect: { - en: 'Your current password is incorrect.', - args: undefined, + en: "Your current password is incorrect.", + args: undefined, }, passwordDescription: { - en: 'Require password to unlock Session.', - args: undefined, + en: "Require password to unlock Session.", + args: undefined, }, passwordEnter: { - en: 'Enter password', - args: undefined, + en: "Enter password", + args: undefined, }, passwordEnterCurrent: { - en: 'Please enter your current password', - args: undefined, + en: "Please enter your current password", + args: undefined, }, passwordEnterNew: { - en: 'Please enter your new password', - args: undefined, + en: "Please enter your new password", + args: undefined, }, passwordError: { - en: 'Password must only contain letters, numbers and symbols', - args: undefined, + en: "Password must only contain letters, numbers and symbols", + args: undefined, }, passwordErrorLength: { - en: 'Password must be between 6 and 64 characters long', - args: undefined, + en: "Password must be between 6 and 64 characters long", + args: undefined, }, passwordErrorMatch: { - en: 'Passwords do not match', - args: undefined, + en: "Passwords do not match", + args: undefined, }, passwordFailed: { - en: 'Failed to set password', - args: undefined, + en: "Failed to set password", + args: undefined, }, passwordIncorrect: { - en: 'Incorrect password', - args: undefined, + en: "Incorrect password", + args: undefined, }, passwordRemove: { - en: 'Remove Password', - args: undefined, + en: "Remove Password", + args: undefined, }, passwordRemoveDescription: { - en: 'Remove the password required to unlock Session.', - args: undefined, + en: "Remove the password required to unlock Session.", + args: undefined, }, passwordRemovedDescription: { - en: 'Your password has been removed.', - args: undefined, + en: "Your password has been removed.", + args: undefined, }, passwordSet: { - en: 'Set Password', - args: undefined, + en: "Set Password", + args: undefined, }, passwordSetDescription: { - en: 'Your password has been set. Please keep it safe.', - args: undefined, + en: "Your password has been set. Please keep it safe.", + args: undefined, }, paste: { - en: 'Paste', - args: undefined, + en: "Paste", + args: undefined, }, permissionChange: { - en: 'Permission Change', - args: undefined, + en: "Permission Change", + args: undefined, }, permissionMusicAudioDenied: { - en: 'Session needs music and audio access in order to send files, music and audio, but it has been permanently denied. Tap Settings → Permissions, and turn "Music and audio" on.', - args: undefined, + en: "Session needs music and audio access in order to send files, music and audio, but it has been permanently denied. Tap Settings → Permissions, and turn \"Music and audio\" on.", + args: undefined, }, permissionsAppleMusic: { - en: 'Session needs to use Apple Music to play media attachments.', - args: undefined, + en: "Session needs to use Apple Music to play media attachments.", + args: undefined, }, permissionsAutoUpdate: { - en: 'Auto Update', - args: undefined, + en: "Auto Update", + args: undefined, }, permissionsAutoUpdateDescription: { - en: 'Automatically check for updates on startup.', - args: undefined, + en: "Automatically check for updates on startup.", + args: undefined, }, permissionsCameraAccessRequiredCallsIos: { - en: 'Camera access is required to make video calls. Toggle the "Camera" permission in Settings to continue.', - args: undefined, + en: "Camera access is required to make video calls. Toggle the \"Camera\" permission in Settings to continue.", + args: undefined, }, permissionsCameraChangeDescriptionIos: { - en: 'Camera access is currently enabled. To disable it, toggle the "Camera" permission in Settings.', - args: undefined, + en: "Camera access is currently enabled. To disable it, toggle the \"Camera\" permission in Settings.", + args: undefined, }, permissionsCameraDenied: { - en: 'Session needs camera access to take photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn "Camera" on.', - args: undefined, + en: "Session needs camera access to take photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn \"Camera\" on.", + args: undefined, }, permissionsCameraDescriptionIos: { - en: 'Allow access to camera for video calls.', - args: undefined, + en: "Allow access to camera for video calls.", + args: undefined, }, permissionsFaceId: { - en: 'The screen lock feature on Session uses Face ID.', - args: undefined, + en: "The screen lock feature on Session uses Face ID.", + args: undefined, }, permissionsKeepInSystemTray: { - en: 'Keep in System Tray', - args: undefined, + en: "Keep in System Tray", + args: undefined, }, permissionsKeepInSystemTrayDescription: { - en: 'Session continues running in the background when you close the window', - args: undefined, + en: "Session continues running in the background when you close the window", + args: undefined, }, permissionsLibrary: { - en: 'Session needs photo library access to continue. You can enable access in the iOS settings.', - args: undefined, + en: "Session needs photo library access to continue. You can enable access in the iOS settings.", + args: undefined, }, permissionsLocalNetworkAccessRequiredCallsIos: { - en: 'Local Network access is required to facilitate calls. Toggle the "Local Network" permission in Settings to continue.', - args: undefined, + en: "Local Network access is required to facilitate calls. Toggle the \"Local Network\" permission in Settings to continue.", + args: undefined, }, permissionsLocalNetworkAccessRequiredIos: { - en: 'Session needs access to local network to make voice and video calls.', - args: undefined, + en: "Session needs access to local network to make voice and video calls.", + args: undefined, }, permissionsLocalNetworkChangeDescriptionIos: { - en: 'Local Network access is currently enabled. To disable it, toggle the "Local Network" permission in Settings.', - args: undefined, + en: "Local Network access is currently enabled. To disable it, toggle the \"Local Network\" permission in Settings.", + args: undefined, }, permissionsLocalNetworkDescriptionIos: { - en: 'Allow access to local network to facilitate voice and video calls.', - args: undefined, + en: "Allow access to local network to facilitate voice and video calls.", + args: undefined, }, permissionsLocalNetworkIos: { - en: 'Local Network', - args: undefined, + en: "Local Network", + args: undefined, }, permissionsMicrophone: { - en: 'Microphone', - args: undefined, + en: "Microphone", + args: undefined, }, permissionsMicrophoneAccessRequired: { - en: 'Session needs microphone access to make calls and send audio messages, but it has been permanently denied. Tap settings → Permissions, and turn "Microphone" on.', - args: undefined, + en: "Session needs microphone access to make calls and send audio messages, but it has been permanently denied. Tap settings → Permissions, and turn \"Microphone\" on.", + args: undefined, }, permissionsMicrophoneAccessRequiredCallsIos: { - en: 'Microphone access is required to make calls and record audio messages. Toggle the "Microphone" permission in Settings to continue.', - args: undefined, + en: "Microphone access is required to make calls and record audio messages. Toggle the \"Microphone\" permission in Settings to continue.", + args: undefined, }, permissionsMicrophoneAccessRequiredDesktop: { - en: "You can enable microphone access in Session's privacy settings", - args: undefined, + en: "You can enable microphone access in Session's privacy settings", + args: undefined, }, permissionsMicrophoneAccessRequiredIos: { - en: 'Session needs microphone access to make calls and record audio messages.', - args: undefined, + en: "Session needs microphone access to make calls and record audio messages.", + args: undefined, }, permissionsMicrophoneChangeDescriptionIos: { - en: 'Microphone access is currently enabled. To disable it, toggle the "Microphone" permission in Settings.', - args: undefined, + en: "Microphone access is currently enabled. To disable it, toggle the \"Microphone\" permission in Settings.", + args: undefined, }, permissionsMicrophoneDescription: { - en: 'Allow access to microphone.', - args: undefined, + en: "Allow access to microphone.", + args: undefined, }, permissionsMicrophoneDescriptionIos: { - en: 'Allow access to microphone for voice calls and audio messages.', - args: undefined, + en: "Allow access to microphone for voice calls and audio messages.", + args: undefined, }, permissionsMusicAudio: { - en: 'Session needs music and audio access in order to send files, music and audio.', - args: undefined, + en: "Session needs music and audio access in order to send files, music and audio.", + args: undefined, }, permissionsRequired: { - en: 'Permission Required', - args: undefined, + en: "Permission Required", + args: undefined, }, permissionsStorageDenied: { - en: 'Session needs photo library access so you can send photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn "Photos and videos" on.', - args: undefined, + en: "Session needs photo library access so you can send photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn \"Photos and videos\" on.", + args: undefined, }, permissionsStorageDeniedLegacy: { - en: 'Session needs storage access so you can send and save attachments. Tap Settings → Permissions, and turn "Storage" on.', - args: undefined, + en: "Session needs storage access so you can send and save attachments. Tap Settings → Permissions, and turn \"Storage\" on.", + args: undefined, }, permissionsStorageSave: { - en: 'Session needs storage access to save attachments and media.', - args: undefined, + en: "Session needs storage access to save attachments and media.", + args: undefined, }, permissionsStorageSaveDenied: { - en: 'Session needs storage access to save photos and videos, but it has been permanently denied. Please continue to app settings, select "Permissions", and enable "Storage".', - args: undefined, + en: "Session needs storage access to save photos and videos, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Storage\".", + args: undefined, }, permissionsStorageSend: { - en: 'Session needs storage access to send photos and videos.', - args: undefined, + en: "Session needs storage access to send photos and videos.", + args: undefined, }, permissionsWriteCommunity: { - en: "You don't have write permissions in this community", - args: undefined, + en: "You don't have write permissions in this community", + args: undefined, }, pin: { - en: 'Pin', - args: undefined, + en: "Pin", + args: undefined, }, pinConversation: { - en: 'Pin Conversation', - args: undefined, + en: "Pin Conversation", + args: undefined, }, pinUnpin: { - en: 'Unpin', - args: undefined, + en: "Unpin", + args: undefined, }, pinUnpinConversation: { - en: 'Unpin Conversation', - args: undefined, + en: "Unpin Conversation", + args: undefined, }, preview: { - en: 'Preview', - args: undefined, + en: "Preview", + args: undefined, }, profile: { - en: 'Profile', - args: undefined, + en: "Profile", + args: undefined, }, profileDisplayPicture: { - en: 'Display Picture', - args: undefined, + en: "Display Picture", + args: undefined, }, profileDisplayPictureRemoveError: { - en: 'Failed to remove display picture.', - args: undefined, + en: "Failed to remove display picture.", + args: undefined, }, profileDisplayPictureSet: { - en: 'Set Display Picture', - args: undefined, + en: "Set Display Picture", + args: undefined, }, profileDisplayPictureSizeError: { - en: 'Please pick a smaller file.', - args: undefined, + en: "Please pick a smaller file.", + args: undefined, }, profileErrorUpdate: { - en: 'Failed to update profile.', - args: undefined, + en: "Failed to update profile.", + args: undefined, }, promote: { - en: 'Promote', - args: undefined, + en: "Promote", + args: undefined, }, qrCode: { - en: 'QR Code', - args: undefined, + en: "QR Code", + args: undefined, }, qrNotAccountId: { - en: 'This QR code does not contain an Account ID', - args: undefined, + en: "This QR code does not contain an Account ID", + args: undefined, }, qrNotRecoveryPassword: { - en: 'This QR code does not contain a Recovery Password', - args: undefined, + en: "This QR code does not contain a Recovery Password", + args: undefined, }, qrScan: { - en: 'Scan QR Code', - args: undefined, + en: "Scan QR Code", + args: undefined, }, qrView: { - en: 'View QR', - args: undefined, + en: "View QR", + args: undefined, }, qrYoursDescription: { - en: 'Friends can message you by scanning your QR code.', - args: undefined, + en: "Friends can message you by scanning your QR code.", + args: undefined, }, quit: { - en: 'Quit Session', - args: undefined, + en: "Quit Session", + args: undefined, }, quitButton: { - en: 'Quit', - args: undefined, + en: "Quit", + args: undefined, }, read: { - en: 'Read', - args: undefined, + en: "Read", + args: undefined, }, readReceipts: { - en: 'Read Receipts', - args: undefined, + en: "Read Receipts", + args: undefined, }, readReceiptsDescription: { - en: 'Show read receipts for all messages you send and receive.', - args: undefined, + en: "Show read receipts for all messages you send and receive.", + args: undefined, }, received: { - en: 'Received:', - args: undefined, + en: "Received:", + args: undefined, }, receivedAnswer: { - en: 'Received Answer', - args: undefined, + en: "Received Answer", + args: undefined, }, receivingCallOffer: { - en: 'Receiving Call Offer', - args: undefined, + en: "Receiving Call Offer", + args: undefined, }, receivingPreOffer: { - en: 'Receiving Pre Offer', - args: undefined, + en: "Receiving Pre Offer", + args: undefined, }, recommended: { - en: 'Recommended', - args: undefined, + en: "Recommended", + args: undefined, }, recoveryPasswordBannerDescription: { - en: "Save your recovery password to make sure you don't lose access to your account.", - args: undefined, + en: "Save your recovery password to make sure you don't lose access to your account.", + args: undefined, }, recoveryPasswordBannerTitle: { - en: 'Save your recovery password', - args: undefined, + en: "Save your recovery password", + args: undefined, }, recoveryPasswordDescription: { - en: "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.", - args: undefined, + en: "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.", + args: undefined, }, recoveryPasswordEnter: { - en: 'Enter your recovery password', - args: undefined, + en: "Enter your recovery password", + args: undefined, }, recoveryPasswordErrorLoad: { - en: 'An error occurred when trying to load your recovery password.

Please export your logs, then upload the file through the Session Help Desk to help resolve this issue.', - args: undefined, + en: "An error occurred when trying to load your recovery password.

Please export your logs, then upload the file through the Session Help Desk to help resolve this issue.", + args: undefined, }, recoveryPasswordErrorMessageGeneric: { - en: 'Please check your recovery password and try again.', - args: undefined, + en: "Please check your recovery password and try again.", + args: undefined, }, recoveryPasswordErrorMessageIncorrect: { - en: 'Some of the words in your Recovery Password are incorrect. Please check and try again.', - args: undefined, + en: "Some of the words in your Recovery Password are incorrect. Please check and try again.", + args: undefined, }, recoveryPasswordErrorMessageShort: { - en: 'The Recovery Password you entered is not long enough. Please check and try again.', - args: undefined, + en: "The Recovery Password you entered is not long enough. Please check and try again.", + args: undefined, }, recoveryPasswordErrorTitle: { - en: 'Incorrect Recovery Password', - args: undefined, + en: "Incorrect Recovery Password", + args: undefined, }, recoveryPasswordExplanation: { - en: 'To load your account, enter your recovery password.', - args: undefined, + en: "To load your account, enter your recovery password.", + args: undefined, }, recoveryPasswordHidePermanently: { - en: 'Hide Recovery Password Permanently', - args: undefined, + en: "Hide Recovery Password Permanently", + args: undefined, }, recoveryPasswordHidePermanentlyDescription1: { - en: 'Without your recovery password, you cannot load your account on new devices.

We strongly recommend you save your recovery password in a safe and secure place before continuing.', - args: undefined, + en: "Without your recovery password, you cannot load your account on new devices.

We strongly recommend you save your recovery password in a safe and secure place before continuing.", + args: undefined, }, recoveryPasswordHidePermanentlyDescription2: { - en: 'Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.', - args: undefined, + en: "Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.", + args: undefined, }, recoveryPasswordHideRecoveryPassword: { - en: 'Hide Recovery Password', - args: undefined, + en: "Hide Recovery Password", + args: undefined, }, recoveryPasswordHideRecoveryPasswordDescription: { - en: 'Permanently hide your recovery password on this device.', - args: undefined, + en: "Permanently hide your recovery password on this device.", + args: undefined, }, recoveryPasswordRestoreDescription: { - en: "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.", - args: undefined, + en: "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.", + args: undefined, }, recoveryPasswordView: { - en: 'View Password', - args: undefined, + en: "View Password", + args: undefined, }, recoveryPasswordWarningSendDescription: { - en: "This is your recovery password. If you send it to someone they'll have full access to your account.", - args: undefined, + en: "This is your recovery password. If you send it to someone they'll have full access to your account.", + args: undefined, }, recreateGroup: { - en: 'Recreate Group', - args: undefined, + en: "Recreate Group", + args: undefined, }, redo: { - en: 'Redo', - args: undefined, + en: "Redo", + args: undefined, }, remainingCharactersOverTooltip: { - en: 'Message is too long', - args: undefined, - }, - remainingCharactersTooltip: { - en: '{count} characters remaining', - args: { count: 'number' }, + en: "Reduce message length by {count}", + args: {count: "number"} }, remove: { - en: 'Remove', - args: undefined, + en: "Remove", + args: undefined, }, removePasswordFail: { - en: 'Failed to remove password', - args: undefined, + en: "Failed to remove password", + args: undefined, }, reply: { - en: 'Reply', - args: undefined, + en: "Reply", + args: undefined, }, resend: { - en: 'Resend', - args: undefined, + en: "Resend", + args: undefined, }, resolving: { - en: 'Loading country information...', - args: undefined, + en: "Loading country information...", + args: undefined, }, restart: { - en: 'Restart', - args: undefined, + en: "Restart", + args: undefined, }, resync: { - en: 'Resync', - args: undefined, + en: "Resync", + args: undefined, }, retry: { - en: 'Retry', - args: undefined, + en: "Retry", + args: undefined, }, save: { - en: 'Save', - args: undefined, + en: "Save", + args: undefined, }, saved: { - en: 'Saved', - args: undefined, + en: "Saved", + args: undefined, }, savedMessages: { - en: 'Saved messages', - args: undefined, + en: "Saved messages", + args: undefined, }, saving: { - en: 'Saving...', - args: undefined, + en: "Saving...", + args: undefined, }, scan: { - en: 'Scan', - args: undefined, + en: "Scan", + args: undefined, }, screenSecurity: { - en: 'Screen Security', - args: undefined, + en: "Screen Security", + args: undefined, }, screenshotNotifications: { - en: 'Screenshot Notifications', - args: undefined, + en: "Screenshot Notifications", + args: undefined, }, screenshotNotificationsDescription: { - en: 'Require a notification when a contact takes a screenshot of a one-to-one chat.', - args: undefined, + en: "Require a notification when a contact takes a screenshot of a one-to-one chat.", + args: undefined, }, screenshotTaken: { - en: '{name} took a screenshot.', - args: { name: 'string' }, + en: "{name} took a screenshot.", + args: {name: "string"} }, search: { - en: 'Search', - args: undefined, + en: "Search", + args: undefined, }, searchContacts: { - en: 'Search Contacts', - args: undefined, + en: "Search Contacts", + args: undefined, }, searchConversation: { - en: 'Search Conversation', - args: undefined, + en: "Search Conversation", + args: undefined, }, searchEnter: { - en: 'Please enter your search.', - args: undefined, + en: "Please enter your search.", + args: undefined, }, searchMatchesNone: { - en: 'No results found.', - args: undefined, + en: "No results found.", + args: undefined, }, searchMatchesNoneSpecific: { - en: 'No results found for {query}', - args: { query: 'string' }, + en: "No results found for {query}", + args: {query: "string"} }, searchMembers: { - en: 'Search Members', - args: undefined, + en: "Search Members", + args: undefined, }, searchSearching: { - en: 'Searching...', - args: undefined, + en: "Searching...", + args: undefined, }, select: { - en: 'Select', - args: undefined, + en: "Select", + args: undefined, }, selectAll: { - en: 'Select All', - args: undefined, + en: "Select All", + args: undefined, }, selectAppIcon: { - en: 'Select app icon', - args: undefined, + en: "Select app icon", + args: undefined, }, send: { - en: 'Send', - args: undefined, + en: "Send", + args: undefined, }, sending: { - en: 'Sending', - args: undefined, + en: "Sending", + args: undefined, }, sendingCallOffer: { - en: 'Sending Call Offer', - args: undefined, + en: "Sending Call Offer", + args: undefined, }, sendingConnectionCandidates: { - en: 'Sending Connection Candidates', - args: undefined, + en: "Sending Connection Candidates", + args: undefined, }, sent: { - en: 'Sent:', - args: undefined, + en: "Sent:", + args: undefined, }, sessionAppearance: { - en: 'Appearance', - args: undefined, + en: "Appearance", + args: undefined, }, sessionClearData: { - en: 'Clear Data', - args: undefined, + en: "Clear Data", + args: undefined, }, sessionConversations: { - en: 'Conversations', - args: undefined, + en: "Conversations", + args: undefined, }, sessionHelp: { - en: 'Help', - args: undefined, + en: "Help", + args: undefined, }, sessionInviteAFriend: { - en: 'Invite a Friend', - args: undefined, + en: "Invite a Friend", + args: undefined, }, sessionMessageRequests: { - en: 'Message Requests', - args: undefined, + en: "Message Requests", + args: undefined, }, sessionNetworkCurrentPrice: { - en: 'Current SESH price', - args: undefined, + en: "Current SESH price", + args: undefined, }, 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}', - args: { icon: 'string' }, + 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}", + args: {icon: "string"} }, sessionNetworkLearnAboutStaking: { - en: 'Learn About Staking', - args: undefined, + en: "Learn About Staking", + args: undefined, }, sessionNetworkMarketCap: { - en: 'Market Cap', - args: undefined, + en: "Market Cap", + args: undefined, }, sessionNetworkNodesSecuring: { - en: 'Session Nodes securing your messages', - args: undefined, + en: "Session Nodes securing your messages", + args: undefined, }, sessionNetworkNodesSwarm: { - en: 'Session Nodes in your swarm', - args: undefined, + en: "Session Nodes in your swarm", + args: undefined, }, sessionNetworkNotificationLive: { - en: 'Session Token is live! Explore the new Session Network section in Settings to learn how Session Token powers Session.', - args: undefined, + en: "Session Token is live! Explore the new Session Network section in Settings to learn how Session Token powers Session.", + args: undefined, }, sessionNetworkSecuredBy: { - en: 'Network secured by', - args: undefined, + en: "Network secured by", + args: undefined, }, sessionNetworkTokenDescription: { - en: 'When you stake Session Token to secure the network, you earn rewards in SESH from the Staking Reward Pool.', - args: undefined, + en: "When you stake Session Token to secure the network, you earn rewards in SESH from the Staking Reward Pool.", + args: undefined, }, sessionNew: { - en: '• New', - args: undefined, + en: "• New", + args: undefined, }, sessionNotifications: { - en: 'Notifications', - args: undefined, + en: "Notifications", + args: undefined, }, sessionPermissions: { - en: 'Permissions', - args: undefined, + en: "Permissions", + args: undefined, }, sessionPrivacy: { - en: 'Privacy', - args: undefined, + en: "Privacy", + args: undefined, }, sessionRecoveryPassword: { - en: 'Recovery Password', - args: undefined, + en: "Recovery Password", + args: undefined, }, sessionSettings: { - en: 'Settings', - args: undefined, + en: "Settings", + args: undefined, }, set: { - en: 'Set', - args: undefined, + en: "Set", + args: undefined, }, setCommunityDisplayPicture: { - en: 'Set Community Display Picture', - args: undefined, + en: "Set Community Display Picture", + args: undefined, }, settingsRestartDescription: { - en: 'You must restart Session to apply your new settings.', - args: undefined, + en: "You must restart Session to apply your new settings.", + args: undefined, }, share: { - en: 'Share', - args: undefined, + en: "Share", + args: undefined, }, shareAccountIdDescription: { - en: 'Invite your friend to chat with you on Session by sharing your Account ID with them.', - args: undefined, + en: "Invite your friend to chat with you on Session by sharing your Account ID with them.", + args: undefined, }, shareAccountIdDescriptionCopied: { - en: 'Share with your friends wherever you usually speak with them — then move the conversation here.', - args: undefined, + en: "Share with your friends wherever you usually speak with them — then move the conversation here.", + args: undefined, }, shareExtensionDatabaseError: { - en: 'There is an issue opening the database. Please restart the app and try again.', - args: undefined, + en: "There is an issue opening the database. Please restart the app and try again.", + args: undefined, }, shareExtensionNoAccountError: { - en: "Oops! Looks like you don't have a Session account yet.

You'll need to create one in the Session app before you can share.", - args: undefined, + en: "Oops! Looks like you don't have a Session account yet.

You'll need to create one in the Session app before you can share.", + args: undefined, }, shareToSession: { - en: 'Share to Session', - args: undefined, + en: "Share to Session", + args: undefined, }, show: { - en: 'Show', - args: undefined, + en: "Show", + args: undefined, }, showAll: { - en: 'Show All', - args: undefined, + en: "Show All", + args: undefined, }, showLess: { - en: 'Show Less', - args: undefined, + en: "Show Less", + args: undefined, }, showNoteToSelf: { - en: 'Show Note to Self', - args: undefined, + en: "Show Note to Self", + args: undefined, }, showNoteToSelfDescription: { - en: 'Are you sure you want to show Note to Self in your conversation list?', - args: undefined, + en: "Are you sure you want to show Note to Self in your conversation list?", + args: undefined, }, stickers: { - en: 'Stickers', - args: undefined, + en: "Stickers", + args: undefined, }, supportGoTo: { - en: 'Go to Support Page', - args: undefined, + en: "Go to Support Page", + args: undefined, }, systemInformationDesktop: { - en: 'System Information: {information}', - args: { information: 'string' }, + en: "System Information: {information}", + args: {information: "string"} }, tapToRetry: { - en: 'Tap to retry', - args: undefined, + en: "Tap to retry", + args: undefined, }, theContinue: { - en: 'Continue', - args: undefined, + en: "Continue", + args: undefined, }, theDefault: { - en: 'Default', - args: undefined, + en: "Default", + args: undefined, }, theError: { - en: 'Error', - args: undefined, + en: "Error", + args: undefined, }, tryAgain: { - en: 'Try Again', - args: undefined, + en: "Try Again", + args: undefined, }, typingIndicators: { - en: 'Typing Indicators', - args: undefined, + en: "Typing Indicators", + args: undefined, }, typingIndicatorsDescription: { - en: 'See and share typing indicators.', - args: undefined, + en: "See and share typing indicators.", + args: undefined, }, unavailable: { - en: 'Unavailable', - args: undefined, + en: "Unavailable", + args: undefined, }, undo: { - en: 'Undo', - args: undefined, + en: "Undo", + args: undefined, }, unknown: { - en: 'Unknown', - args: undefined, + en: "Unknown", + args: undefined, }, updateApp: { - en: 'App updates', - args: undefined, + en: "App updates", + args: undefined, }, updateDownloaded: { - en: 'Update installed, click to restart', - args: undefined, + en: "Update installed, click to restart", + args: undefined, }, updateDownloading: { - en: 'Downloading update: {percent_loader}%', - args: { percent_loader: 'string' }, + en: "Downloading update: {percent_loader}%", + args: {percent_loader: "string"} }, updateError: { - en: 'Cannot Update', - args: undefined, + en: "Cannot Update", + args: undefined, }, updateErrorDescription: { - en: 'Session failed to update. Please go to https://getsession.org/download and install the new version manually, then contact our Help Center to let us know about this problem.', - args: undefined, + en: "Session failed to update. Please go to https://getsession.org/download and install the new version manually, then contact our Help Center to let us know about this problem.", + args: undefined, }, updateGroupInformation: { - en: 'Update Group Information', - args: undefined, + en: "Update Group Information", + args: undefined, }, updateGroupInformationDescription: { - en: 'Group name and description are visible to all group members.', - args: undefined, + en: "Group name and description are visible to all group members.", + args: undefined, }, updateGroupInformationEnterShorterDescription: { - en: 'Please enter a shorter group description', - args: undefined, + en: "Please enter a shorter group description", + args: undefined, }, updateNewVersion: { - en: 'A new version of Session is available, tap to update', - args: undefined, + en: "A new version of Session is available, tap to update", + args: undefined, }, updateNewVersionDescription: { - en: 'A new version ({version}) of Session is available.', - args: { version: 'string' }, + en: "A new version ({version}) of Session is available.", + args: {version: "string"} }, updateReleaseNotes: { - en: 'Go to Release Notes', - args: undefined, + en: "Go to Release Notes", + args: undefined, }, updateSession: { - en: 'Session Update', - args: undefined, + en: "Session Update", + args: undefined, }, updateVersion: { - en: 'Version {version}', - args: { version: 'string' }, + en: "Version {version}", + args: {version: "string"} }, updated: { - en: 'Last updated {relative_time} ago', - args: { relative_time: 'string' }, + en: "Last updated {relative_time} ago", + args: {relative_time: "string"} }, uploading: { - en: 'Uploading', - args: undefined, + en: "Uploading", + args: undefined, }, urlCopy: { - en: 'Copy URL', - args: undefined, + en: "Copy URL", + args: undefined, }, urlOpen: { - en: 'Open URL', - args: undefined, + en: "Open URL", + args: undefined, }, urlOpenBrowser: { - en: 'This will open in your browser.', - args: undefined, + en: "This will open in your browser.", + args: undefined, }, urlOpenDescription: { - en: 'Are you sure you want to open this URL in your browser?

{url}', - args: { url: 'string' }, + en: "Are you sure you want to open this URL in your browser?

{url}", + args: {url: "string"} }, useFastMode: { - en: 'Use Fast Mode', - args: undefined, + en: "Use Fast Mode", + args: undefined, }, video: { - en: 'Video', - args: undefined, + en: "Video", + args: undefined, }, videoErrorPlay: { - en: 'Unable to play video.', - args: undefined, + en: "Unable to play video.", + args: undefined, }, view: { - en: 'View', - args: undefined, + en: "View", + args: undefined, }, viewLess: { - en: 'View Less', - args: undefined, + en: "View Less", + args: undefined, }, viewMore: { - en: 'View More', - args: undefined, + en: "View More", + args: undefined, }, waitFewMinutes: { - en: 'This can take a few minutes.', - args: undefined, + en: "This can take a few minutes.", + args: undefined, }, waitOneMoment: { - en: 'One moment please...', - args: undefined, + en: "One moment please...", + args: undefined, }, warning: { - en: 'Warning', - args: undefined, + en: "Warning", + args: undefined, }, window: { - en: 'Window', - args: undefined, + en: "Window", + args: undefined, }, yes: { - en: 'Yes', - args: undefined, + en: "Yes", + args: undefined, }, you: { - en: 'You', - args: undefined, + en: "You", + args: undefined, + }, + sessionNetworkDataPrice: { + en: "Price data powered by CoinGecko
Accurate at {date_time}", + args: {date_time: "string"} }, } as const; export const pluralsDictionary = { adminSendingPromotion: { - en: { - one: 'Sending admin promotion', - other: 'Sending admin promotions', + en:{ + one: "Sending admin promotion", + other: "Sending admin promotions" }, - args: { count: 'number' }, + args: {count: "number"} }, clearDataErrorDescription: { - en: { - one: 'Data not deleted by {count} Service Node. Service Node ID: {service_node_id}.', - other: 'Data not deleted by {count} Service Nodes. Service Node IDs: {service_node_id}.', + en:{ + one: "Data not deleted by {count} Service Node. Service Node ID: {service_node_id}.", + other: "Data not deleted by {count} Service Nodes. Service Node IDs: {service_node_id}." }, - args: { count: 'number', service_node_id: 'string' }, + args: {count: "number", service_node_id: "string"} }, deleteMessage: { - en: { - one: 'Delete Message', - other: 'Delete Messages', + en:{ + one: "Delete Message", + other: "Delete Messages" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageConfirm: { - en: { - one: 'Are you sure you want to delete this message?', - other: 'Are you sure you want to delete these messages?', + en:{ + one: "Are you sure you want to delete this message?", + other: "Are you sure you want to delete these messages?" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageDeleted: { - en: { - one: 'Message deleted', - other: 'Messages deleted', + en:{ + one: "Message deleted", + other: "Messages deleted" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageDescriptionDevice: { - en: { - one: 'Are you sure you want to delete this message from this device only?', - other: 'Are you sure you want to delete these messages from this device only?', + en:{ + one: "Are you sure you want to delete this message from this device only?", + other: "Are you sure you want to delete these messages from this device only?" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageFailed: { - en: { - one: 'Failed to delete message', - other: 'Failed to delete messages', + en:{ + one: "Failed to delete message", + other: "Failed to delete messages" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageNoteToSelfWarning: { - en: { - one: 'This message cannot be deleted from all your devices', - other: 'Some of the messages you have selected cannot be deleted from all your devices', + en:{ + one: "This message cannot be deleted from all your devices", + other: "Some of the messages you have selected cannot be deleted from all your devices" }, - args: { count: 'number' }, + args: {count: "number"} }, deleteMessageWarning: { - en: { - one: 'This message cannot be deleted for everyone', - other: 'Some of the messages you have selected cannot be deleted for everyone', + en:{ + one: "This message cannot be deleted for everyone", + other: "Some of the messages you have selected cannot be deleted for everyone" }, - args: { count: 'number' }, + args: {count: "number"} }, emojiReactsCountOthers: { - en: { - one: 'And {count} other has reacted {emoji} to this message.', - other: 'And {count} others have reacted {emoji} to this message.', + en:{ + one: "And {count} other has reacted {emoji} to this message.", + other: "And {count} others have reacted {emoji} to this message." }, - args: { count: 'number', emoji: 'string' }, + args: {count: "number", emoji: "string"} }, groupInviteSending: { - en: { - one: 'Sending invite', - other: 'Sending invites', + en:{ + one: "Sending invite", + other: "Sending invites" }, - args: { count: 'number' }, + args: {count: "number"} }, groupRemoveMessages: { - en: { - one: 'Remove user and their messages', - other: 'Remove users and their messages', + en:{ + one: "Remove user and their messages", + other: "Remove users and their messages" }, - args: { count: 'number' }, + args: {count: "number"} }, groupRemoveUserOnly: { - en: { - one: 'Remove user', - other: 'Remove users', + en:{ + one: "Remove user", + other: "Remove users" }, - args: { count: 'number' }, + args: {count: "number"} }, inviteFailed: { - en: { - one: 'Invite Failed', - other: 'Invites Failed', + en:{ + one: "Invite Failed", + other: "Invites Failed" }, - args: { count: 'number' }, + args: {count: "number"} }, inviteFailedDescription: { - en: { - one: 'The invite could not be sent. Would you like to try again?', - other: 'The invites could not be sent. Would you like to try again?', + en:{ + one: "The invite could not be sent. Would you like to try again?", + other: "The invites could not be sent. Would you like to try again?" }, - args: { count: 'number' }, + args: {count: "number"} }, members: { - en: { - one: '{count} member', - other: '{count} members', + en:{ + one: "{count} member", + other: "{count} members" }, - args: { count: 'number' }, + args: {count: "number"} }, membersActive: { - en: { - one: '{count} active member', - other: '{count} active members', + en:{ + one: "{count} active member", + other: "{count} active members" }, - args: { count: 'number' }, + args: {count: "number"} }, membersInviteSend: { - en: { - one: 'Send Invite', - other: 'Send Invites', + en:{ + one: "Send Invite", + other: "Send Invites" }, - args: { count: 'number' }, + args: {count: "number"} }, messageNew: { - en: { - one: 'New Message', - other: 'New Messages', + en:{ + one: "New Message", + other: "New Messages" }, - args: { count: 'number' }, + args: {count: "number"} }, messageNewYouveGot: { - en: { + en:{ one: "You've got a new message.", - other: "You've got {count} new messages.", + other: "You've got {count} new messages." }, - args: { count: 'number' }, + args: {count: "number"} }, messageNewYouveGotGroup: { - en: { + en:{ one: "You've got a new message in {group_name}.", - other: "You've got {count} new messages in {group_name}.", + other: "You've got {count} new messages in {group_name}." + }, + args: {group_name: "string", count: "number"} + }, + modalMessageCharacterDisplayDescription: { + en:{ + one: "Messages have a character limit of {limit} characters. You have {count} character remaining", + other: "Messages have a character limit of {limit} characters. You have {count} characters remaining" }, - args: { group_name: 'string', count: 'number' }, + args: {limit: "string", count: "number"} }, promotionFailed: { - en: { - one: 'Promotion Failed', - other: 'Promotions Failed', + en:{ + one: "Promotion Failed", + other: "Promotions Failed" }, - args: { count: 'number' }, + args: {count: "number"} }, promotionFailedDescription: { - en: { - one: 'The promotion could not be applied. Would you like to try again?', - other: 'The promotions could not be applied. Would you like to try again?', + en:{ + one: "The promotion could not be applied. Would you like to try again?", + other: "The promotions could not be applied. Would you like to try again?" + }, + args: {count: "number"} + }, + remainingCharactersTooltip: { + en:{ + one: "{count} character remaining", + other: "{count} characters remaining" }, - args: { count: 'number' }, + args: {count: "number"} }, searchMatches: { - en: { - one: '{found_count} of {count} match', - other: '{found_count} of {count} matches', + en:{ + one: "{found_count} of {count} match", + other: "{found_count} of {count} matches" }, - args: { found_count: 'number', count: 'number' }, + args: {found_count: "number", count: "number"} }, } as const; From 3b1ec517f6bd4513a24d9b2aade27629570e53d9 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Jun 2025 16:44:39 +1000 Subject: [PATCH 18/60] WIP: Android 1.25.0 changes --- run/localizer/constants.ts | 5 +- run/localizer/locales.ts | 3791 ++++++++--------- run/test/specs/check_avatar_color.spec.ts | 30 +- run/test/specs/community_tests_join.spec.ts | 4 +- .../disappearing_community_invite.spec.ts | 17 +- run/test/specs/disappearing_link.spec.ts | 9 +- .../specs/linked_device_create_group.spec.ts | 8 +- .../specs/linked_device_restore_group.spec.ts | 9 +- run/test/specs/locators/conversation.ts | 31 +- run/test/specs/locators/groups.ts | 16 + run/test/specs/locators/index.ts | 3 +- run/test/specs/locators/settings.ts | 4 +- .../message_community_invitation.spec.ts | 13 +- run/test/specs/message_deletion.spec.ts | 2 +- .../specs/message_requests_accept.spec.ts | 1 + .../specs/message_requests_delete.spec.ts | 4 +- ...er_actions_block_conversation_list.spec.ts | 12 +- ...actions_block_conversation_options.spec.ts | 5 +- .../user_actions_share_to_session.spec.ts | 11 +- .../specs/user_actions_unblock_user.spec.ts | 5 +- run/test/specs/utils/create_account.ts | 4 +- run/test/specs/utils/join_community.ts | 9 +- run/types/DeviceWrapper.ts | 4 +- run/types/testing.ts | 10 +- 24 files changed, 2011 insertions(+), 1996 deletions(-) diff --git a/run/localizer/constants.ts b/run/localizer/constants.ts index fd7dc29d..6fd2b45b 100644 --- a/run/localizer/constants.ts +++ b/run/localizer/constants.ts @@ -13,13 +13,10 @@ export enum LOCALE_DEFAULTS { 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 b538d48b..3ec1b4ae 100644 --- a/run/localizer/locales.ts +++ b/run/localizer/locales.ts @@ -1,3776 +1,3775 @@ - // This file was generated by a script. Do not modify this file manually. // To make changes, modify the corresponding JSON file and re-run the script. - export const simpleDictionary = { about: { - en: "About", - args: undefined, + en: 'About', + args: undefined, }, accept: { - en: "Accept", - args: undefined, + en: 'Accept', + args: undefined, }, accountIDCopy: { - en: "Copy Account ID", - args: undefined, + en: 'Copy Account ID', + args: undefined, }, accountId: { - en: "Account ID", - args: undefined, + en: 'Account ID', + args: undefined, }, accountIdCopied: { - en: "Account ID Copied", - args: undefined, + en: 'Account ID Copied', + args: undefined, }, accountIdCopyDescription: { - en: "Copy your Account ID then share it with your friends so they can message you.", - args: undefined, + en: 'Copy your Account ID then share it with your friends so they can message you.', + args: undefined, }, accountIdEnter: { - en: "Enter Account ID", - args: undefined, + en: 'Enter Account ID', + args: undefined, }, accountIdErrorInvalid: { - en: "This Account ID is invalid. Please check and try again.", - args: undefined, + en: 'This Account ID is invalid. Please check and try again.', + args: undefined, }, accountIdOrOnsEnter: { - en: "Enter Account ID or ONS", - args: undefined, + en: 'Enter Account ID or ONS', + args: undefined, }, accountIdOrOnsInvite: { - en: "Invite Account ID or ONS", - args: undefined, + en: 'Invite Account ID or ONS', + args: undefined, }, accountIdShare: { - en: "Hey, I've been using Session to chat with complete privacy and security. Come join me! My Account ID is

{account_id}

Download it at https://getsession.org/download", - args: {account_id: "string"} + en: "Hey, I've been using Session to chat with complete privacy and security. Come join me! My Account ID is

{account_id}

Download it at https://getsession.org/download", + args: { account_id: 'string' }, }, accountIdYours: { - en: "Your Account ID", - args: undefined, + en: 'Your Account ID', + args: undefined, }, accountIdYoursDescription: { - en: "This is your Account ID. Other users can scan it to start a conversation with you.", - args: undefined, + en: 'This is your Account ID. Other users can scan it to start a conversation with you.', + args: undefined, }, actualSize: { - en: "Actual Size", - args: undefined, + en: 'Actual Size', + args: undefined, }, add: { - en: "Add", - args: undefined, + en: 'Add', + args: undefined, }, addAdmins: { - en: "Add Admins", - args: undefined, + en: 'Add Admins', + args: undefined, }, addAdminsDescription: { - en: "Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time.", - args: undefined, + en: 'Enter the Account ID of the user you are promoting to admin.

To add multiple users, enter each Account ID separated by a comma. Up to 20 Account IDs can be specified at a time.', + args: undefined, }, adminCannotBeRemoved: { - en: "Admins cannot be removed.", - args: undefined, + en: 'Admins cannot be removed.', + args: undefined, }, adminMorePromotedToAdmin: { - en: "{name} and {count} others were promoted to Admin.", - args: {name: "string", count: "number"} + en: '{name} and {count} others were promoted to Admin.', + args: { name: 'string', count: 'number' }, }, adminPromote: { - en: "Promote Admins", - args: undefined, + en: 'Promote Admins', + args: undefined, }, adminPromoteDescription: { - en: "Are you sure you want to promote {name} to admin? Admins cannot be removed.", - args: {name: "string"} + en: 'Are you sure you want to promote {name} to admin? Admins cannot be removed.', + args: { name: 'string' }, }, adminPromoteMoreDescription: { - en: "Are you sure you want to promote {name} and {count} others to admin? Admins cannot be removed.", - args: {name: "string", count: "number"} + en: 'Are you sure you want to promote {name} and {count} others to admin? Admins cannot be removed.', + args: { name: 'string', count: 'number' }, }, adminPromoteToAdmin: { - en: "Promote to Admin", - args: undefined, + en: 'Promote to Admin', + args: undefined, }, adminPromoteTwoDescription: { - en: "Are you sure you want to promote {name} and {other_name} to admin? Admins cannot be removed.", - args: {name: "string", other_name: "string"} + en: 'Are you sure you want to promote {name} and {other_name} to admin? Admins cannot be removed.', + args: { name: 'string', other_name: 'string' }, }, adminPromotedToAdmin: { - en: "{name} was promoted to Admin.", - args: {name: "string"} + en: '{name} was promoted to Admin.', + args: { name: 'string' }, }, adminPromotionFailed: { - en: "Admin promotion failed", - args: undefined, + en: 'Admin promotion failed', + args: undefined, }, adminPromotionFailedDescription: { - en: "Failed to promote {name} in {group_name}", - args: {name: "string", group_name: "string"} + en: 'Failed to promote {name} in {group_name}', + args: { name: 'string', group_name: 'string' }, }, adminPromotionFailedDescriptionMultiple: { - en: "Failed to promote {name} and {count} others in {group_name}", - args: {name: "string", count: "number", group_name: "string"} + en: 'Failed to promote {name} and {count} others in {group_name}', + args: { name: 'string', count: 'number', group_name: 'string' }, }, adminPromotionFailedDescriptionTwo: { - en: "Failed to promote {name} and {other_name} in {group_name}", - args: {name: "string", other_name: "string", group_name: "string"} + en: 'Failed to promote {name} and {other_name} in {group_name}', + args: { name: 'string', other_name: 'string', group_name: 'string' }, }, adminPromotionNotSent: { - en: "Promotion not sent", - args: undefined, + en: 'Promotion not sent', + args: undefined, }, adminPromotionSent: { - en: "Admin promotion sent", - args: undefined, + en: 'Admin promotion sent', + args: undefined, }, adminPromotionStatusUnknown: { - en: "Promotion status unknown", - args: undefined, + en: 'Promotion status unknown', + args: undefined, }, adminRemove: { - en: "Remove Admins", - args: undefined, + en: 'Remove Admins', + args: undefined, }, adminRemoveAsAdmin: { - en: "Remove as Admin", - args: undefined, + en: 'Remove as Admin', + args: undefined, }, adminRemoveCommunityNone: { - en: "There are no Admins in this Community.", - args: undefined, + en: 'There are no Admins in this Community.', + args: undefined, }, adminRemoveFailed: { - en: "Failed to remove {name} as Admin.", - args: {name: "string"} + en: 'Failed to remove {name} as Admin.', + args: { name: 'string' }, }, adminRemoveFailedMultiple: { - en: "Failed to remove {name} and {count} others as Admin.", - args: {name: "string", count: "number"} + en: 'Failed to remove {name} and {count} others as Admin.', + args: { name: 'string', count: 'number' }, }, adminRemoveFailedOther: { - en: "Failed to remove {name} and {other_name} as Admin.", - args: {name: "string", other_name: "string"} + en: 'Failed to remove {name} and {other_name} as Admin.', + args: { name: 'string', other_name: 'string' }, }, adminRemovedUser: { - en: "{name} was removed as Admin.", - args: {name: "string"} + en: '{name} was removed as Admin.', + args: { name: 'string' }, }, adminRemovedUserMultiple: { - en: "{name} and {count} others were removed as Admin.", - args: {name: "string", count: "number"} + en: '{name} and {count} others were removed as Admin.', + args: { name: 'string', count: 'number' }, }, adminRemovedUserOther: { - en: "{name} and {other_name} were removed as Admin.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} were removed as Admin.', + args: { name: 'string', other_name: 'string' }, }, adminSettings: { - en: "Admin Settings", - args: undefined, + en: 'Admin Settings', + args: undefined, }, adminTwoPromotedToAdmin: { - en: "{name} and {other_name} were promoted to Admin.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} were promoted to Admin.', + args: { name: 'string', other_name: 'string' }, }, andMore: { - en: "+{count}", - args: {count: "number"} + en: '+{count}', + args: { count: 'number' }, }, anonymous: { - en: "Anonymous", - args: undefined, + en: 'Anonymous', + args: undefined, }, appIcon: { - en: "App Icon", - args: undefined, + en: 'App Icon', + args: undefined, }, appIconAndNameChange: { - en: "Change App Icon and Name", - args: undefined, + en: 'Change App Icon and Name', + args: undefined, }, appIconAndNameChangeConfirmation: { - en: "Changing the app icon and name requires Session to be closed. Notifications will continue to use the default Session icon and name.", - args: undefined, + en: 'Changing the app icon and name requires Session to be closed. Notifications will continue to use the default Session icon and name.', + args: undefined, }, appIconAndNameDescription: { - en: "Alternate app icon and name is displayed on home screen and app drawer.", - args: undefined, + en: 'Alternate app icon and name is displayed on home screen and app drawer.', + args: undefined, }, appIconAndNameSelectionDescription: { - en: "The selected app icon and name is displayed on the home screen and app drawer.", - args: undefined, + en: 'The selected app icon and name is displayed on the home screen and app drawer.', + args: undefined, }, appIconAndNameSelectionTitle: { - en: "Icon and name", - args: undefined, + en: 'Icon and name', + args: undefined, }, appIconDescription: { - en: "Alternate app icon is displayed on home screen and app library. App name will still appear as 'Session'.", - args: undefined, + en: "Alternate app icon is displayed on home screen and app library. App name will still appear as 'Session'.", + args: undefined, }, appIconEnableIcon: { - en: "Use alternate app icon", - args: undefined, + en: 'Use alternate app icon', + args: undefined, }, appIconEnableIconAndName: { - en: "Use alternate app icon and name", - args: undefined, + en: 'Use alternate app icon and name', + args: undefined, }, appIconSelect: { - en: "Select alternate app icon", - args: undefined, + en: 'Select alternate app icon', + args: undefined, }, appIconSelectionTitle: { - en: "Icon", - args: undefined, + en: 'Icon', + args: undefined, }, appNameCalculator: { - en: "Calculator", - args: undefined, + en: 'Calculator', + args: undefined, }, appNameMeetingSE: { - en: "MeetingSE", - args: undefined, + en: 'MeetingSE', + args: undefined, }, appNameNews: { - en: "News", - args: undefined, + en: 'News', + args: undefined, }, appNameNotes: { - en: "Notes", - args: undefined, + en: 'Notes', + args: undefined, }, appNameStocks: { - en: "Stocks", - args: undefined, + en: 'Stocks', + args: undefined, }, appNameWeather: { - en: "Weather", - args: undefined, + en: 'Weather', + args: undefined, }, appearanceAutoDarkMode: { - en: "Auto dark-mode", - args: undefined, + en: 'Auto dark-mode', + args: undefined, }, appearanceHideMenuBar: { - en: "Hide Menu Bar", - args: undefined, + en: 'Hide Menu Bar', + args: undefined, }, appearanceLanguage: { - en: "Language", - args: undefined, + en: 'Language', + args: undefined, }, appearanceLanguageDescription: { - en: "Choose your language setting for Session. Session will restart when you change your language setting.", - args: undefined, + en: 'Choose your language setting for Session. Session will restart when you change your language setting.', + args: undefined, }, appearancePreview1: { - en: "How are you?", - args: undefined, + en: 'How are you?', + args: undefined, }, appearancePreview2: { - en: "I'm good thanks, you?", - args: undefined, + en: "I'm good thanks, you?", + args: undefined, }, appearancePreview3: { - en: "I'm doing great, thanks.", - args: undefined, + en: "I'm doing great, thanks.", + args: undefined, }, appearancePrimaryColor: { - en: "Primary Color", - args: undefined, + en: 'Primary Color', + args: undefined, }, appearanceThemes: { - en: "Themes", - args: undefined, + en: 'Themes', + args: undefined, }, appearanceThemesClassicDark: { - en: "Classic Dark", - args: undefined, + en: 'Classic Dark', + args: undefined, }, appearanceThemesClassicLight: { - en: "Classic Light", - args: undefined, + en: 'Classic Light', + args: undefined, }, appearanceThemesOceanDark: { - en: "Ocean Dark", - args: undefined, + en: 'Ocean Dark', + args: undefined, }, appearanceThemesOceanLight: { - en: "Ocean Light", - args: undefined, + en: 'Ocean Light', + args: undefined, }, appearanceZoom: { - en: "Zoom", - args: undefined, + en: 'Zoom', + args: undefined, }, appearanceZoomIn: { - en: "Zoom In", - args: undefined, + en: 'Zoom In', + args: undefined, }, appearanceZoomOut: { - en: "Zoom Out", - args: undefined, + en: 'Zoom Out', + args: undefined, }, attachment: { - en: "Attachment", - args: undefined, + en: 'Attachment', + args: undefined, }, attachments: { - en: "Attachments", - args: undefined, + en: 'Attachments', + args: undefined, }, attachmentsAdd: { - en: "Add attachment", - args: undefined, + en: 'Add attachment', + args: undefined, }, attachmentsAlbumUnnamed: { - en: "Unnamed Album", - args: undefined, + en: 'Unnamed Album', + args: undefined, }, attachmentsAutoDownload: { - en: "Auto-download Attachments", - args: undefined, + en: 'Auto-download Attachments', + args: undefined, }, attachmentsAutoDownloadDescription: { - en: "Automatically download media and files from this chat.", - args: undefined, + en: 'Automatically download media and files from this chat.', + args: undefined, }, attachmentsAutoDownloadModalDescription: { - en: "Would you like to automatically download all files from {conversation_name}?", - args: {conversation_name: "string"} + en: 'Would you like to automatically download all files from {conversation_name}?', + args: { conversation_name: 'string' }, }, attachmentsAutoDownloadModalTitle: { - en: "Auto Download", - args: undefined, + en: 'Auto Download', + args: undefined, }, attachmentsClearAll: { - en: "Clear All Attachments", - args: undefined, + en: 'Clear All Attachments', + args: undefined, }, attachmentsClearAllDescription: { - en: "Are you sure you want to clear all attachments? Messages with attachments will also be deleted.", - args: undefined, + en: 'Are you sure you want to clear all attachments? Messages with attachments will also be deleted.', + args: undefined, }, attachmentsClickToDownload: { - en: "Click to download {file_type}", - args: {file_type: "string"} + en: 'Click to download {file_type}', + args: { file_type: 'string' }, }, attachmentsCollapseOptions: { - en: "Collapse attachment options", - args: undefined, + en: 'Collapse attachment options', + args: undefined, }, attachmentsCollecting: { - en: "Collecting attachments...", - args: undefined, + en: 'Collecting attachments...', + args: undefined, }, attachmentsDownload: { - en: "Download Attachment", - args: undefined, + en: 'Download Attachment', + args: undefined, }, attachmentsDuration: { - en: "Duration:", - args: undefined, + en: 'Duration:', + args: undefined, }, attachmentsErrorLoad: { - en: "Error attaching file", - args: undefined, + en: 'Error attaching file', + args: undefined, }, attachmentsErrorMediaSelection: { - en: "Failed to select attachment", - args: undefined, + en: 'Failed to select attachment', + args: undefined, }, attachmentsErrorNoApp: { - en: "Can't find an app to select media.", - args: undefined, + en: "Can't find an app to select media.", + args: undefined, }, attachmentsErrorNotSupported: { - en: "This file type is not supported.", - args: undefined, + en: 'This file type is not supported.', + args: undefined, }, attachmentsErrorNumber: { - en: "Unable to send more than 32 image and video files at once.", - args: undefined, + en: 'Unable to send more than 32 image and video files at once.', + args: undefined, }, attachmentsErrorOpen: { - en: "Unable to open file.", - args: undefined, + en: 'Unable to open file.', + args: undefined, }, attachmentsErrorSending: { - en: "Error sending file", - args: undefined, + en: 'Error sending file', + args: undefined, }, attachmentsErrorSeparate: { - en: "Please send files as separate messages.", - args: undefined, + en: 'Please send files as separate messages.', + args: undefined, }, attachmentsErrorSize: { - en: "Files must be less than 10MB", - args: undefined, + en: 'Files must be less than 10MB', + args: undefined, }, attachmentsErrorTypes: { - en: "Cannot attach images and video with other file types. Try sending other files in a separate message.", - args: undefined, + en: 'Cannot attach images and video with other file types. Try sending other files in a separate message.', + args: undefined, }, attachmentsExpired: { - en: "Attachment expired", - args: undefined, + en: 'Attachment expired', + args: undefined, }, attachmentsFileId: { - en: "File ID:", - args: undefined, + en: 'File ID:', + args: undefined, }, attachmentsFileSize: { - en: "File Size:", - args: undefined, + en: 'File Size:', + args: undefined, }, attachmentsFileType: { - en: "File Type:", - args: undefined, + en: 'File Type:', + args: undefined, }, attachmentsFilesEmpty: { - en: "You don't have any files in this conversation.", - args: undefined, + en: "You don't have any files in this conversation.", + args: undefined, }, attachmentsImageErrorMetadata: { - en: "Unable to remove metadata from file.", - args: undefined, + en: 'Unable to remove metadata from file.', + args: undefined, }, attachmentsLoadingNewer: { - en: "Loading Newer Media...", - args: undefined, + en: 'Loading Newer Media...', + args: undefined, }, attachmentsLoadingNewerFiles: { - en: "Loading Newer Files...", - args: undefined, + en: 'Loading Newer Files...', + args: undefined, }, attachmentsLoadingOlder: { - en: "Loading Older Media...", - args: undefined, + en: 'Loading Older Media...', + args: undefined, }, attachmentsLoadingOlderFiles: { - en: "Loading Older Files...", - args: undefined, + en: 'Loading Older Files...', + args: undefined, }, attachmentsMedia: { - en: "{name} on {date_time}", - args: {name: "string", date_time: "string"} + en: '{name} on {date_time}', + args: { name: 'string', date_time: 'string' }, }, attachmentsMediaEmpty: { - en: "You don't have any media in this conversation.", - args: undefined, + en: "You don't have any media in this conversation.", + args: undefined, }, attachmentsMediaSaved: { - en: "Media saved by {name}", - args: {name: "string"} + en: 'Media saved by {name}', + args: { name: 'string' }, }, attachmentsMoveAndScale: { - en: "Move and Scale", - args: undefined, + en: 'Move and Scale', + args: undefined, }, attachmentsNa: { - en: "N/A", - args: undefined, + en: 'N/A', + args: undefined, }, attachmentsNotification: { - en: "{emoji} Attachment", - args: {emoji: "string"} + en: '{emoji} Attachment', + args: { emoji: 'string' }, }, attachmentsNotificationGroup: { - en: "{author}: {emoji} Attachment", - args: {author: "string", emoji: "string"} + en: '{author}: {emoji} Attachment', + args: { author: 'string', emoji: 'string' }, }, attachmentsResolution: { - en: "Resolution:", - args: undefined, + en: 'Resolution:', + args: undefined, }, attachmentsSaveError: { - en: "Unable to save file.", - args: undefined, + en: 'Unable to save file.', + args: undefined, }, attachmentsSendTo: { - en: "Send to {name}", - args: {name: "string"} + en: 'Send to {name}', + args: { name: 'string' }, }, attachmentsTapToDownload: { - en: "Tap to download {file_type}", - args: {file_type: "string"} + en: 'Tap to download {file_type}', + args: { file_type: 'string' }, }, attachmentsThisMonth: { - en: "This Month", - args: undefined, + en: 'This Month', + args: undefined, }, attachmentsThisWeek: { - en: "This Week", - args: undefined, + en: 'This Week', + args: undefined, }, attachmentsWarning: { - en: "Attachments you save can be accessed by other apps on your device.", - args: undefined, + en: 'Attachments you save can be accessed by other apps on your device.', + args: undefined, }, audio: { - en: "Audio", - args: undefined, + en: 'Audio', + args: undefined, }, audioNoInput: { - en: "No audio input found", - args: undefined, + en: 'No audio input found', + args: undefined, }, audioNoOutput: { - en: "No audio output found", - args: undefined, + en: 'No audio output found', + args: undefined, }, audioUnableToPlay: { - en: "Unable to play audio file.", - args: undefined, + en: 'Unable to play audio file.', + args: undefined, }, audioUnableToRecord: { - en: "Unable to record audio.", - args: undefined, + en: 'Unable to record audio.', + args: undefined, }, authenticateFailed: { - en: "Authentication Failed", - args: undefined, + en: 'Authentication Failed', + args: undefined, }, authenticateFailedTooManyAttempts: { - en: "Too many failed authentication attempts. Please try again later.", - args: undefined, + en: 'Too many failed authentication attempts. Please try again later.', + args: undefined, }, authenticateNotAccessed: { - en: "Authentication could not be accessed.", - args: undefined, + en: 'Authentication could not be accessed.', + args: undefined, }, authenticateToOpen: { - en: "Authenticate to open Session.", - args: undefined, + en: 'Authenticate to open Session.', + args: undefined, }, back: { - en: "Back", - args: undefined, + en: 'Back', + args: undefined, }, banDeleteAll: { - en: "Ban and Delete All", - args: undefined, + en: 'Ban and Delete All', + args: undefined, }, banErrorFailed: { - en: "Ban failed", - args: undefined, + en: 'Ban failed', + args: undefined, }, banUnbanErrorFailed: { - en: "Unban failed", - args: undefined, + en: 'Unban failed', + args: undefined, }, banUnbanUser: { - en: "Unban User", - args: undefined, + en: 'Unban User', + args: undefined, }, banUnbanUserDescription: { - en: "Enter the Account ID of the user you are unbanning", - args: undefined, + en: 'Enter the Account ID of the user you are unbanning', + args: undefined, }, banUnbanUserUnbanned: { - en: "User unbanned", - args: undefined, + en: 'User unbanned', + args: undefined, }, banUser: { - en: "Ban User", - args: undefined, + en: 'Ban User', + args: undefined, }, banUserBanned: { - en: "User banned", - args: undefined, + en: 'User banned', + args: undefined, }, banUserDescription: { - en: "Enter the Account ID of the user you are banning", - args: undefined, + en: 'Enter the Account ID of the user you are banning', + args: undefined, }, block: { - en: "Block", - args: undefined, + en: 'Block', + args: undefined, }, blockBlockedDescription: { - en: "Unblock this contact to send a message", - args: undefined, + en: 'Unblock this contact to send a message', + args: undefined, }, blockBlockedNone: { - en: "No blocked contacts", - args: undefined, + en: 'No blocked contacts', + args: undefined, }, blockBlockedUser: { - en: "Blocked {name}", - args: {name: "string"} + en: 'Blocked {name}', + args: { name: 'string' }, }, blockDescription: { - en: "Are you sure you want to block {name}? Blocked users cannot send you message requests, group invites or call you.", - args: {name: "string"} + en: 'Are you sure you want to block {name}? Blocked users cannot send you message requests, group invites or call you.', + args: { name: 'string' }, }, blockUnblock: { - en: "Unblock", - args: undefined, + en: 'Unblock', + args: undefined, }, blockUnblockName: { - en: "Are you sure you want to unblock {name}?", - args: {name: "string"} + en: 'Are you sure you want to unblock {name}?', + args: { name: 'string' }, }, blockUnblockNameMultiple: { - en: "Are you sure you want to unblock {name} and {count} others?", - args: {name: "string", count: "number"} + en: 'Are you sure you want to unblock {name} and {count} others?', + args: { name: 'string', count: 'number' }, }, blockUnblockNameTwo: { - en: "Are you sure you want to unblock {name} and 1 other?", - args: {name: "string"} + en: 'Are you sure you want to unblock {name} and 1 other?', + args: { name: 'string' }, }, blockUnblockedUser: { - en: "Unblocked {name}", - args: {name: "string"} + en: 'Unblocked {name}', + args: { name: 'string' }, }, call: { - en: "Call", - args: undefined, + en: 'Call', + args: undefined, }, callsCalledYou: { - en: "{name} called you", - args: {name: "string"} + en: '{name} called you', + args: { name: 'string' }, }, callsCannotStart: { - en: "You cannot start a new call. Finish your current call first.", - args: undefined, + en: 'You cannot start a new call. Finish your current call first.', + args: undefined, }, callsConnecting: { - en: "Connecting...", - args: undefined, + en: 'Connecting...', + args: undefined, }, callsEnd: { - en: "End call", - args: undefined, + en: 'End call', + args: undefined, }, callsEnded: { - en: "Call Ended", - args: undefined, + en: 'Call Ended', + args: undefined, }, callsErrorAnswer: { - en: "Failed to answer call", - args: undefined, + en: 'Failed to answer call', + args: undefined, }, callsErrorStart: { - en: "Failed to start call", - args: undefined, + en: 'Failed to start call', + args: undefined, }, callsInProgress: { - en: "Call in progress", - args: undefined, + en: 'Call in progress', + args: undefined, }, callsIncoming: { - en: "Incoming call from {name}", - args: {name: "string"} + en: 'Incoming call from {name}', + args: { name: 'string' }, }, callsIncomingUnknown: { - en: "Incoming call", - args: undefined, + en: 'Incoming call', + args: undefined, }, callsMicrophonePermissionsRequired: { - en: "You missed a call from {name} because you haven't granted microphone access.", - args: {name: "string"} + en: "You missed a call from {name} because you haven't granted microphone access.", + args: { name: 'string' }, }, callsMissed: { - en: "Missed Call", - args: undefined, + en: 'Missed Call', + args: undefined, }, callsMissedCallFrom: { - en: "Missed call from {name}", - args: {name: "string"} + en: 'Missed call from {name}', + args: { name: 'string' }, }, callsNotificationsRequired: { - en: "Voice and Video Calls require notifications to be enabled in your device system settings.", - args: undefined, + en: 'Voice and Video Calls require notifications to be enabled in your device system settings.', + args: undefined, }, callsPermissionsRequired: { - en: "Call Permissions Required", - args: undefined, + en: 'Call Permissions Required', + args: undefined, }, callsPermissionsRequiredDescription: { - en: "You can enable the \"Voice and Video Calls\" permission in Privacy Settings.", - args: undefined, + en: 'You can enable the "Voice and Video Calls" permission in Privacy Settings.', + args: undefined, }, callsPermissionsRequiredDescription1: { - en: "You can enable the \"Voice and Video Calls\" permission in Permissions Settings.", - args: undefined, + en: 'You can enable the "Voice and Video Calls" permission in Permissions Settings.', + args: undefined, }, callsReconnecting: { - en: "Reconnecting…", - args: undefined, + en: 'Reconnecting…', + args: undefined, }, callsRinging: { - en: "Ringing...", - args: undefined, + en: 'Ringing...', + args: undefined, }, callsSessionCall: { - en: "Session Call", - args: undefined, + en: 'Session Call', + args: undefined, }, callsSettings: { - en: "Calls (Beta)", - args: undefined, + en: 'Calls (Beta)', + args: undefined, }, callsVoiceAndVideo: { - en: "Voice and Video Calls", - args: undefined, + en: 'Voice and Video Calls', + args: undefined, }, callsVoiceAndVideoBeta: { - en: "Voice and Video Calls (Beta)", - args: undefined, + en: 'Voice and Video Calls (Beta)', + args: undefined, }, callsVoiceAndVideoModalDescription: { - en: "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.", - args: undefined, + en: 'Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.', + args: undefined, }, callsVoiceAndVideoToggleDescription: { - en: "Enables voice and video calls to and from other users.", - args: undefined, + en: 'Enables voice and video calls to and from other users.', + args: undefined, }, callsYouCalled: { - en: "You called {name}", - args: {name: "string"} + en: 'You called {name}', + args: { name: 'string' }, }, callsYouMissedCallPermissions: { - en: "You missed a call from {name} because you haven't enabled Voice and Video Calls in Privacy Settings.", - args: {name: "string"} + en: "You missed a call from {name} because you haven't enabled Voice and Video Calls in Privacy Settings.", + args: { name: 'string' }, }, cameraErrorNotFound: { - en: "No camera found", - args: undefined, + en: 'No camera found', + args: undefined, }, cameraErrorUnavailable: { - en: "Camera unavailable.", - args: undefined, + en: 'Camera unavailable.', + args: undefined, }, cameraGrantAccess: { - en: "Grant Camera Access", - args: undefined, + en: 'Grant Camera Access', + args: undefined, }, cameraGrantAccessDenied: { - en: "Session needs camera access to take photos and videos, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".", - args: undefined, + en: 'Session needs camera access to take photos and videos, but it has been permanently denied. Please continue to app settings, select "Permissions", and enable "Camera".', + args: undefined, }, cameraGrantAccessDescription: { - en: "Session needs camera access to take photos and videos, or scan QR codes.", - args: undefined, + en: 'Session needs camera access to take photos and videos, or scan QR codes.', + args: undefined, }, cameraGrantAccessQr: { - en: "Session needs camera access to scan QR codes", - args: undefined, + en: 'Session needs camera access to scan QR codes', + args: undefined, }, cancel: { - en: "Cancel", - args: undefined, + en: 'Cancel', + args: undefined, }, changePasswordFail: { - en: "Failed to change password", - args: undefined, + en: 'Failed to change password', + args: undefined, }, clear: { - en: "Clear", - args: undefined, + en: 'Clear', + args: undefined, }, clearAll: { - en: "Clear All", - args: undefined, + en: 'Clear All', + args: undefined, }, clearDataAll: { - en: "Clear All Data", - args: undefined, + en: 'Clear All Data', + args: undefined, }, clearDataAllDescription: { - en: "This will permanently delete your messages and contacts. Would you like to clear this device only, or delete your data from the network as well?", - args: undefined, + en: 'This will permanently delete your messages and contacts. Would you like to clear this device only, or delete your data from the network as well?', + args: undefined, }, clearDataError: { - en: "Data Not Deleted", - args: undefined, + en: 'Data Not Deleted', + args: undefined, }, clearDataErrorDescriptionGeneric: { - en: "An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead?", - args: undefined, + en: 'An unknown error occurred and your data was not deleted. Do you want to delete your data from just this device instead?', + args: undefined, }, clearDevice: { - en: "Clear Device", - args: undefined, + en: 'Clear Device', + args: undefined, }, clearDeviceAndNetwork: { - en: "Clear device and network", - args: undefined, + en: 'Clear device and network', + args: undefined, }, clearDeviceAndNetworkConfirm: { - en: "Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.", - args: undefined, + en: 'Are you sure you want to delete your data from the network? If you continue, you will not be able to restore your messages or contacts.', + args: undefined, }, clearDeviceDescription: { - en: "Are you sure you want to clear your device?", - args: undefined, + en: 'Are you sure you want to clear your device?', + args: undefined, }, clearDeviceOnly: { - en: "Clear device only", - args: undefined, + en: 'Clear device only', + args: undefined, }, clearDeviceRestart: { - en: "Clear Device and Restart", - args: undefined, + en: 'Clear Device and Restart', + args: undefined, }, clearDeviceRestore: { - en: "Clear Device and Restore", - args: undefined, + en: 'Clear Device and Restore', + args: undefined, }, clearMessages: { - en: "Clear All Messages", - args: undefined, + en: 'Clear All Messages', + args: undefined, }, clearMessagesChatDescription: { - en: "Are you sure you want to clear all messages from your conversation with {name} from your device?", - args: {name: "string"} + en: 'Are you sure you want to clear all messages from your conversation with {name} from your device?', + args: { name: 'string' }, }, clearMessagesChatDescriptionUpdated: { - en: "Are you sure you want to clear all messages from your conversation with {name} on this device?", - args: {name: "string"} + en: 'Are you sure you want to clear all messages from your conversation with {name} on this device?', + args: { name: 'string' }, }, clearMessagesCommunity: { - en: "Are you sure you want to clear all {community_name} messages from your device?", - args: {community_name: "string"} + en: 'Are you sure you want to clear all {community_name} messages from your device?', + args: { community_name: 'string' }, }, clearMessagesCommunityUpdated: { - en: "Are you sure you want to clear all messages from {community_name} on this device?", - args: {community_name: "string"} + en: 'Are you sure you want to clear all messages from {community_name} on this device?', + args: { community_name: 'string' }, }, clearMessagesForEveryone: { - en: "Clear for everyone", - args: undefined, + en: 'Clear for everyone', + args: undefined, }, clearMessagesForMe: { - en: "Clear for me", - args: undefined, + en: 'Clear for me', + args: undefined, }, clearMessagesGroupAdminDescription: { - en: "Are you sure you want to clear all {group_name} messages?", - args: {group_name: "string"} + en: 'Are you sure you want to clear all {group_name} messages?', + args: { group_name: 'string' }, }, clearMessagesGroupAdminDescriptionUpdated: { - en: "Are you sure you want to clear all messages from {group_name}?", - args: {group_name: "string"} + en: 'Are you sure you want to clear all messages from {group_name}?', + args: { group_name: 'string' }, }, clearMessagesGroupDescription: { - en: "Are you sure you want to clear all {group_name} messages from your device?", - args: {group_name: "string"} + en: 'Are you sure you want to clear all {group_name} messages from your device?', + args: { group_name: 'string' }, }, clearMessagesGroupDescriptionUpdated: { - en: "Are you sure you want to clear all messages from {group_name} on this device?", - args: {group_name: "string"} + en: 'Are you sure you want to clear all messages from {group_name} on this device?', + args: { group_name: 'string' }, }, clearMessagesNoteToSelfDescription: { - en: "Are you sure you want to clear all Note to Self messages from your device?", - args: undefined, + en: 'Are you sure you want to clear all Note to Self messages from your device?', + args: undefined, }, clearMessagesNoteToSelfDescriptionUpdated: { - en: "Are you sure you want to clear all Note to Self messages on this device?", - args: undefined, + en: 'Are you sure you want to clear all Note to Self messages on this device?', + args: undefined, }, clearOnThisDevice: { - en: "Clear on this device", - args: undefined, + en: 'Clear on this device', + args: undefined, }, close: { - en: "Close", - args: undefined, + en: 'Close', + args: undefined, }, closeApp: { - en: "Close App", - args: undefined, + en: 'Close App', + args: undefined, }, closeWindow: { - en: "Close Window", - args: undefined, + en: 'Close Window', + args: undefined, }, commitHashDesktop: { - en: "Commit Hash: {hash}", - args: {hash: "string"} + en: 'Commit Hash: {hash}', + args: { hash: 'string' }, }, communityBanDeleteDescription: { - en: "This will ban the selected user from this Community and delete all their messages. Are you sure you want to continue?", - args: undefined, + en: 'This will ban the selected user from this Community and delete all their messages. Are you sure you want to continue?', + args: undefined, }, communityBanDescription: { - en: "This will ban the selected user from this Community. Are you sure you want to continue?", - args: undefined, + en: 'This will ban the selected user from this Community. Are you sure you want to continue?', + args: undefined, }, communityEnterUrl: { - en: "Enter Community URL", - args: undefined, + en: 'Enter Community URL', + args: undefined, }, communityEnterUrlErrorInvalid: { - en: "Invalid URL", - args: undefined, + en: 'Invalid URL', + args: undefined, }, communityEnterUrlErrorInvalidDescription: { - en: "Please check the Community URL and try again.", - args: undefined, + en: 'Please check the Community URL and try again.', + args: undefined, }, communityError: { - en: "Community Error", - args: undefined, + en: 'Community Error', + args: undefined, }, communityErrorDescription: { - en: "Oops, an error occurred. Please try again later.", - args: undefined, + en: 'Oops, an error occurred. Please try again later.', + args: undefined, }, communityInvitation: { - en: "Community Invitation", - args: undefined, + en: 'Community Invitation', + args: undefined, }, communityJoin: { - en: "Join Community", - args: undefined, + en: 'Join Community', + args: undefined, }, communityJoinDescription: { - en: "Are you sure you want to join {community_name}?", - args: {community_name: "string"} + en: 'Are you sure you want to join {community_name}?', + args: { community_name: 'string' }, }, communityJoinError: { - en: "Failed to join community", - args: undefined, + en: 'Failed to join community', + args: undefined, }, communityJoinOfficial: { - en: "Or join one of these...", - args: undefined, + en: 'Or join one of these...', + args: undefined, }, communityJoined: { - en: "Joined Community", - args: undefined, + en: 'Joined Community', + args: undefined, }, communityJoinedAlready: { - en: "You are already a member of this community.", - args: undefined, + en: 'You are already a member of this community.', + args: undefined, }, communityLeave: { - en: "Leave Community", - args: undefined, + en: 'Leave Community', + args: undefined, }, communityLeaveError: { - en: "Failed to leave {community_name}", - args: {community_name: "string"} + en: 'Failed to leave {community_name}', + args: { community_name: 'string' }, }, communityUnknown: { - en: "Unknown Community", - args: undefined, + en: 'Unknown Community', + args: undefined, }, communityUrl: { - en: "Community URL", - args: undefined, + en: 'Community URL', + args: undefined, }, communityUrlCopy: { - en: "Copy Community URL", - args: undefined, + en: 'Copy Community URL', + args: undefined, }, confirm: { - en: "Confirm", - args: undefined, + en: 'Confirm', + args: undefined, }, contactContacts: { - en: "Contacts", - args: undefined, + en: 'Contacts', + args: undefined, }, contactDelete: { - en: "Delete Contact", - args: undefined, + en: 'Delete Contact', + args: undefined, }, contactDeleteDescription: { - en: "Are you sure you want to delete {name} from your contacts? New messages from {name} will arrive as a message request.", - args: {name: "string"} + en: 'Are you sure you want to delete {name} from your contacts? New messages from {name} will arrive as a message request.', + args: { name: 'string' }, }, contactNone: { - en: "You don't have any contacts yet", - args: undefined, + en: "You don't have any contacts yet", + args: undefined, }, contactSelect: { - en: "Select Contacts", - args: undefined, + en: 'Select Contacts', + args: undefined, }, contactUserDetails: { - en: "User Details", - args: undefined, + en: 'User Details', + args: undefined, }, contentDescriptionCamera: { - en: "Camera", - args: undefined, + en: 'Camera', + args: undefined, }, contentDescriptionChooseConversationType: { - en: "Choose an action to start a conversation", - args: undefined, + en: 'Choose an action to start a conversation', + args: undefined, }, contentDescriptionMediaMessage: { - en: "Media message", - args: undefined, + en: 'Media message', + args: undefined, }, contentDescriptionMessageComposition: { - en: "Message composition", - args: undefined, + en: 'Message composition', + args: undefined, }, contentDescriptionQuoteThumbnail: { - en: "Thumbnail of image from quoted message", - args: undefined, + en: 'Thumbnail of image from quoted message', + args: undefined, }, contentDescriptionStartConversation: { - en: "Create a conversation with a new contact", - args: undefined, + en: 'Create a conversation with a new contact', + args: undefined, }, conversationsAddToHome: { - en: "Add to home screen", - args: undefined, + en: 'Add to home screen', + args: undefined, }, conversationsAddedToHome: { - en: "Added to home screen", - args: undefined, + en: 'Added to home screen', + args: undefined, }, conversationsAudioMessages: { - en: "Audio Messages", - args: undefined, + en: 'Audio Messages', + args: undefined, }, conversationsAutoplayAudioMessage: { - en: "Autoplay Audio Messages", - args: undefined, + en: 'Autoplay Audio Messages', + args: undefined, }, conversationsAutoplayAudioMessageDescription: { - en: "Autoplay consecutively sent audio messages.", - args: undefined, + en: 'Autoplay consecutively sent audio messages.', + args: undefined, }, conversationsBlockedContacts: { - en: "Blocked Contacts", - args: undefined, + en: 'Blocked Contacts', + args: undefined, }, conversationsCommunities: { - en: "Communities", - args: undefined, + en: 'Communities', + args: undefined, }, conversationsDelete: { - en: "Delete Conversation", - args: undefined, + en: 'Delete Conversation', + args: undefined, }, conversationsDeleteDescription: { - en: "Are you sure you want to delete your conversation with {name}? New messages from {name} will start a new conversation.", - args: {name: "string"} + en: 'Are you sure you want to delete your conversation with {name}? New messages from {name} will start a new conversation.', + args: { name: 'string' }, }, conversationsDeleted: { - en: "Conversation deleted", - args: undefined, + en: 'Conversation deleted', + args: undefined, }, conversationsEmpty: { - en: "There are no messages in {conversation_name}.", - args: {conversation_name: "string"} + en: 'There are no messages in {conversation_name}.', + args: { conversation_name: 'string' }, }, conversationsEnter: { - en: "Enter Key", - args: undefined, + en: 'Enter Key', + args: undefined, }, conversationsEnterDescription: { - en: "Function of the enter key when typing in a conversation.", - args: undefined, + en: 'Function of the enter key when typing in a conversation.', + args: undefined, }, conversationsEnterNewLine: { - en: "SHIFT + ENTER sends a message, ENTER starts a new line", - args: undefined, + en: 'SHIFT + ENTER sends a message, ENTER starts a new line', + args: undefined, }, conversationsEnterSends: { - en: "ENTER sends a message, SHIFT + ENTER starts a new line", - args: undefined, + en: 'ENTER sends a message, SHIFT + ENTER starts a new line', + args: undefined, }, conversationsGroups: { - en: "Groups", - args: undefined, + en: 'Groups', + args: undefined, }, conversationsMessageTrimming: { - en: "Message Trimming", - args: undefined, + en: 'Message Trimming', + args: undefined, }, conversationsMessageTrimmingTrimCommunities: { - en: "Trim Communities", - args: undefined, + en: 'Trim Communities', + args: undefined, }, conversationsMessageTrimmingTrimCommunitiesDescription: { - en: "Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages.", - args: undefined, + en: 'Delete messages from Community conversations older than 6 months, and where there are over 2,000 messages.', + args: undefined, }, conversationsNew: { - en: "New Conversation", - args: undefined, + en: 'New Conversation', + args: undefined, }, conversationsNone: { - en: "You don't have any conversations yet", - args: undefined, + en: "You don't have any conversations yet", + args: undefined, }, conversationsSendWithEnterKey: { - en: "Send with Enter Key", - args: undefined, + en: 'Send with Enter Key', + args: undefined, }, conversationsSendWithEnterKeyDescription: { - en: "Tapping the Enter Key will send message instead of starting a new line.", - args: undefined, + en: 'Tapping the Enter Key will send message instead of starting a new line.', + args: undefined, }, conversationsSettingsAllMedia: { - en: "All Media", - args: undefined, + en: 'All Media', + args: undefined, }, conversationsSpellCheck: { - en: "Spell Check", - args: undefined, + en: 'Spell Check', + args: undefined, }, conversationsSpellCheckDescription: { - en: "Enable spell check when typing messages.", - args: undefined, + en: 'Enable spell check when typing messages.', + args: undefined, }, conversationsStart: { - en: "Start Conversation", - args: undefined, + en: 'Start Conversation', + args: undefined, }, copied: { - en: "Copied", - args: undefined, + en: 'Copied', + args: undefined, }, copy: { - en: "Copy", - args: undefined, + en: 'Copy', + args: undefined, }, create: { - en: "Create", - args: undefined, + en: 'Create', + args: undefined, }, creatingCall: { - en: "Creating Call", - args: undefined, + en: 'Creating Call', + args: undefined, }, cut: { - en: "Cut", - args: undefined, + en: 'Cut', + args: undefined, }, databaseErrorClearDataWarning: { - en: "Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?", - args: undefined, + en: 'Are you sure you want to delete all messages, attachments, and account data from this device and create a new account?', + args: undefined, }, databaseErrorGeneric: { - en: "A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.", - args: undefined, + en: 'A database error occurred.

Export your application logs to share for troubleshooting. If this is unsuccessful, reinstall Session and restore your account.', + args: undefined, }, databaseErrorRestoreDataWarning: { - en: "Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?", - args: undefined, + en: 'Are you sure you want to delete all messages, attachments, and account data from this device and restore your account from the network?', + args: undefined, }, databaseErrorTimeout: { - en: "We've noticed Session is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session.", - args: undefined, + en: "We've noticed Session is taking a long time to start.

You can continue to wait, export your device logs to share for troubleshooting, or try restarting Session.", + args: undefined, }, databaseErrorUpdate: { - en: "Your app database is incompatible with this version of Session. Reinstall the app and restore your account to generate a new database and continue using Session.

Warning: This will result in the loss of all messages and attachments older than two weeks.", - args: undefined, + en: 'Your app database is incompatible with this version of Session. Reinstall the app and restore your account to generate a new database and continue using Session.

Warning: This will result in the loss of all messages and attachments older than two weeks.', + args: undefined, }, databaseOptimizing: { - en: "Optimizing Database", - args: undefined, + en: 'Optimizing Database', + args: undefined, }, debugLog: { - en: "Debug Log", - args: undefined, + en: 'Debug Log', + args: undefined, }, decline: { - en: "Decline", - args: undefined, + en: 'Decline', + args: undefined, }, delete: { - en: "Delete", - args: undefined, + en: 'Delete', + args: undefined, }, deleteAfterGroupFirstReleaseConfigOutdated: { - en: "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.", - args: undefined, + en: 'Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.', + args: undefined, }, deleteAfterGroupPR1BlockThisUser: { - en: "Block This User", - args: undefined, + en: 'Block This User', + args: undefined, }, deleteAfterGroupPR1BlockUser: { - en: "Block User", - args: undefined, + en: 'Block User', + args: undefined, }, deleteAfterGroupPR1GroupSettings: { - en: "Group Settings", - args: undefined, + en: 'Group Settings', + args: undefined, }, deleteAfterGroupPR1MentionsOnly: { - en: "Notify for Mentions Only", - args: undefined, + en: 'Notify for Mentions Only', + args: undefined, }, deleteAfterGroupPR1MentionsOnlyDescription: { - en: "When enabled, you'll only be notified for messages mentioning you.", - args: undefined, + en: "When enabled, you'll only be notified for messages mentioning you.", + args: undefined, }, deleteAfterGroupPR1MessageSound: { - en: "Message Sound", - args: undefined, + en: 'Message Sound', + args: undefined, }, deleteAfterGroupPR3DeleteMessagesConfirmation: { - en: "Permanently delete the messages in this conversation?", - args: undefined, + en: 'Permanently delete the messages in this conversation?', + args: undefined, }, deleteAfterGroupPR3GroupErrorLeave: { - en: "Can't leave while adding or removing other members.", - args: undefined, + en: "Can't leave while adding or removing other members.", + args: undefined, }, deleteAfterLegacyDisappearingMessagesLegacy: { - en: "Legacy", - args: undefined, + en: 'Legacy', + args: undefined, }, deleteAfterLegacyDisappearingMessagesOriginal: { - en: "Original version of disappearing messages.", - args: undefined, + en: 'Original version of disappearing messages.', + args: undefined, }, deleteAfterLegacyDisappearingMessagesTheyChangedTimer: { - en: "{name} set the disappearing message timer to {time}", - args: {name: "string", time: "string"} + en: '{name} set the disappearing message timer to {time}', + args: { name: 'string', time: 'string' }, }, deleteAfterLegacyGroupsGroupCreation: { - en: "Please wait while the group is created...", - args: undefined, + en: 'Please wait while the group is created...', + args: undefined, }, deleteAfterLegacyGroupsGroupUpdateErrorTitle: { - en: "Failed to Update Group", - args: undefined, + en: 'Failed to Update Group', + args: undefined, }, deleteAfterMessageDeletionStandardisationMessageDeletionForbidden: { - en: "You don’t have permission to delete others’ messages", - args: undefined, + en: 'You don’t have permission to delete others’ messages', + args: undefined, }, deleteContactDescription: { - en: "Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request.", - args: {name: "string"} + en: 'Are you sure you want to delete {name} from your contacts?

This will delete your conversation, including all messages and attachments. Future messages from {name} will appear as a message request.', + args: { name: 'string' }, }, deleteConversationDescription: { - en: "Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments.", - args: {name: "string"} + en: 'Are you sure you want to delete your conversation with {name}?
This will permanently delete all messages and attachments.', + args: { name: 'string' }, }, deleteMessageDeletedGlobally: { - en: "This message was deleted", - args: undefined, + en: 'This message was deleted', + args: undefined, }, deleteMessageDeletedLocally: { - en: "This message was deleted on this device", - args: undefined, + en: 'This message was deleted on this device', + args: undefined, }, deleteMessageDescriptionEveryone: { - en: "Are you sure you want to delete this message for everyone?", - args: undefined, + en: 'Are you sure you want to delete this message for everyone?', + args: undefined, }, deleteMessageDeviceOnly: { - en: "Delete on this device only", - args: undefined, + en: 'Delete on this device only', + args: undefined, }, deleteMessageDevicesAll: { - en: "Delete on all my devices", - args: undefined, + en: 'Delete on all my devices', + args: undefined, }, deleteMessageEveryone: { - en: "Delete for everyone", - args: undefined, + en: 'Delete for everyone', + args: undefined, }, deleteMessagesDescriptionEveryone: { - en: "Are you sure you want to delete these messages for everyone?", - args: undefined, + en: 'Are you sure you want to delete these messages for everyone?', + args: undefined, }, deleting: { - en: "Deleting", - args: undefined, + en: 'Deleting', + args: undefined, }, developerToolsToggle: { - en: "Toggle Developer Tools", - args: undefined, + en: 'Toggle Developer Tools', + args: undefined, }, dictationStart: { - en: "Start Dictation...", - args: undefined, + en: 'Start Dictation...', + args: undefined, }, disappearingMessages: { - en: "Disappearing Messages", - args: undefined, + en: 'Disappearing Messages', + args: undefined, }, disappearingMessagesCountdownBig: { - en: "Message will delete in {time_large}", - args: {time_large: "string"} + en: 'Message will delete in {time_large}', + args: { time_large: 'string' }, }, disappearingMessagesCountdownBigMobile: { - en: "Auto-deletes in {time_large}", - args: {time_large: "string"} + en: 'Auto-deletes in {time_large}', + args: { time_large: 'string' }, }, disappearingMessagesCountdownBigSmall: { - en: "Message will delete in {time_large} {time_small}", - args: {time_large: "string", time_small: "string"} + en: 'Message will delete in {time_large} {time_small}', + args: { time_large: 'string', time_small: 'string' }, }, disappearingMessagesCountdownBigSmallMobile: { - en: "Auto-deletes in {time_large} {time_small}", - args: {time_large: "string", time_small: "string"} + en: 'Auto-deletes in {time_large} {time_small}', + args: { time_large: 'string', time_small: 'string' }, }, disappearingMessagesDeleteType: { - en: "Delete Type", - args: undefined, + en: 'Delete Type', + args: undefined, }, disappearingMessagesDescription: { - en: "This setting applies to everyone in this conversation.", - args: undefined, + en: 'This setting applies to everyone in this conversation.', + args: undefined, }, disappearingMessagesDescription1: { - en: "This setting applies to messages you send in this conversation.", - args: undefined, + en: 'This setting applies to messages you send in this conversation.', + args: undefined, }, disappearingMessagesDescriptionGroup: { - en: "This setting applies to everyone in this conversation.
Only group admins can change this setting.", - args: undefined, + en: 'This setting applies to everyone in this conversation.
Only group admins can change this setting.', + args: undefined, }, disappearingMessagesDisappear: { - en: "Disappear After {disappearing_messages_type} - {time}", - args: {disappearing_messages_type: "string", time: "string"} + en: 'Disappear After {disappearing_messages_type} - {time}', + args: { disappearing_messages_type: 'string', time: 'string' }, }, disappearingMessagesDisappearAfterRead: { - en: "Disappear After Read", - args: undefined, + en: 'Disappear After Read', + args: undefined, }, disappearingMessagesDisappearAfterReadDescription: { - en: "Messages delete after they have been read.", - args: undefined, + en: 'Messages delete after they have been read.', + args: undefined, }, disappearingMessagesDisappearAfterReadState: { - en: "Disappear After Read - {time}", - args: {time: "string"} + en: 'Disappear After Read - {time}', + args: { time: 'string' }, }, disappearingMessagesDisappearAfterSend: { - en: "Disappear After Send", - args: undefined, + en: 'Disappear After Send', + args: undefined, }, disappearingMessagesDisappearAfterSendDescription: { - en: "Messages delete after they have been sent.", - args: undefined, + en: 'Messages delete after they have been sent.', + args: undefined, }, disappearingMessagesDisappearAfterSendState: { - en: "Disappear After Send - {time}", - args: {time: "string"} + en: 'Disappear After Send - {time}', + args: { time: 'string' }, }, disappearingMessagesFollowSetting: { - en: "Follow Setting", - args: undefined, + en: 'Follow Setting', + args: undefined, }, disappearingMessagesFollowSettingOff: { - en: "Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?", - args: undefined, + en: 'Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?', + args: undefined, }, disappearingMessagesFollowSettingOn: { - en: "Set your messages to disappear {time} after they have been {disappearing_messages_type}?", - args: {time: "string", disappearing_messages_type: "string"} + en: 'Set your messages to disappear {time} after they have been {disappearing_messages_type}?', + args: { time: 'string', disappearing_messages_type: 'string' }, }, disappearingMessagesLegacy: { - en: "{name} is using an outdated client. Disappearing messages may not work as expected.", - args: {name: "string"} + en: '{name} is using an outdated client. Disappearing messages may not work as expected.', + args: { name: 'string' }, }, disappearingMessagesOnlyAdmins: { - en: "Only group admins can change this setting.", - args: undefined, + en: 'Only group admins can change this setting.', + args: undefined, }, disappearingMessagesSent: { - en: "Sent", - args: undefined, + en: 'Sent', + args: undefined, }, disappearingMessagesSet: { - en: "{name} has set messages to disappear {time} after they have been {disappearing_messages_type}.", - args: {name: "string", time: "string", disappearing_messages_type: "string"} + en: '{name} has set messages to disappear {time} after they have been {disappearing_messages_type}.', + args: { name: 'string', time: 'string', disappearing_messages_type: 'string' }, }, disappearingMessagesSetYou: { - en: "You set messages to disappear {time} after they have been {disappearing_messages_type}.", - args: {time: "string", disappearing_messages_type: "string"} + en: 'You set messages to disappear {time} after they have been {disappearing_messages_type}.', + args: { time: 'string', disappearing_messages_type: 'string' }, }, disappearingMessagesTimer: { - en: "Timer", - args: undefined, + en: 'Timer', + args: undefined, }, disappearingMessagesTurnedOff: { - en: "{name} has turned disappearing messages off. Messages they send will no longer disappear.", - args: {name: "string"} + en: '{name} has turned disappearing messages off. Messages they send will no longer disappear.', + args: { name: 'string' }, }, disappearingMessagesTurnedOffGroup: { - en: "{name} has turned disappearing messages off.", - args: {name: "string"} + en: '{name} has turned disappearing messages off.', + args: { name: 'string' }, }, disappearingMessagesTurnedOffYou: { - en: "You turned off disappearing messages. Messages you send will no longer disappear.", - args: undefined, + en: 'You turned off disappearing messages. Messages you send will no longer disappear.', + args: undefined, }, disappearingMessagesTurnedOffYouGroup: { - en: "You turned off disappearing messages.", - args: undefined, + en: 'You turned off disappearing messages.', + args: undefined, }, disappearingMessagesTypeRead: { - en: "read", - args: undefined, + en: 'read', + args: undefined, }, disappearingMessagesTypeSent: { - en: "sent", - args: undefined, + en: 'sent', + args: undefined, }, disappearingMessagesUpdated: { - en: "{admin_name} updated disappearing message settings.", - args: {admin_name: "string"} + en: '{admin_name} updated disappearing message settings.', + args: { admin_name: 'string' }, }, disappearingMessagesUpdatedYou: { - en: "You updated disappearing message settings.", - args: undefined, + en: 'You updated disappearing message settings.', + args: undefined, }, dismiss: { - en: "Dismiss", - args: undefined, + en: 'Dismiss', + args: undefined, }, displayNameDescription: { - en: "It can be your real name, an alias, or anything else you like — and you can change it at any time.", - args: undefined, + en: 'It can be your real name, an alias, or anything else you like — and you can change it at any time.', + args: undefined, }, displayNameEnter: { - en: "Enter your display name", - args: undefined, + en: 'Enter your display name', + args: undefined, }, displayNameErrorDescription: { - en: "Please enter a display name", - args: undefined, + en: 'Please enter a display name', + args: undefined, }, displayNameErrorDescriptionShorter: { - en: "Please enter a shorter display name", - args: undefined, + en: 'Please enter a shorter display name', + args: undefined, }, displayNameErrorNew: { - en: "We were unable to load your display name. Please enter a new display name to continue.", - args: undefined, + en: 'We were unable to load your display name. Please enter a new display name to continue.', + args: undefined, }, displayNameNew: { - en: "Pick a new display name", - args: undefined, + en: 'Pick a new display name', + args: undefined, }, displayNamePick: { - en: "Pick your display name", - args: undefined, + en: 'Pick your display name', + args: undefined, }, displayNameSet: { - en: "Set Display Name", - args: undefined, + en: 'Set Display Name', + args: undefined, }, displayNameVisible: { - en: "Your Display Name is visible to users, groups and communities you interact with.", - args: undefined, + en: 'Your Display Name is visible to users, groups and communities you interact with.', + args: undefined, }, document: { - en: "Document", - args: undefined, + en: 'Document', + args: undefined, }, donate: { - en: "Donate", - args: undefined, + en: 'Donate', + args: undefined, }, done: { - en: "Done", - args: undefined, + en: 'Done', + args: undefined, }, download: { - en: "Download", - args: undefined, + en: 'Download', + args: undefined, }, downloading: { - en: "Downloading...", - args: undefined, + en: 'Downloading...', + args: undefined, }, draft: { - en: "Draft", - args: undefined, + en: 'Draft', + args: undefined, }, edit: { - en: "Edit", - args: undefined, + en: 'Edit', + args: undefined, }, emojiAndSymbols: { - en: "Emoji and Symbols", - args: undefined, + en: 'Emoji and Symbols', + args: undefined, }, emojiCategoryActivities: { - en: "Activities", - args: undefined, + en: 'Activities', + args: undefined, }, emojiCategoryAnimals: { - en: "Animals and Nature", - args: undefined, + en: 'Animals and Nature', + args: undefined, }, emojiCategoryFlags: { - en: "Flags", - args: undefined, + en: 'Flags', + args: undefined, }, emojiCategoryFood: { - en: "Food and Drink", - args: undefined, + en: 'Food and Drink', + args: undefined, }, emojiCategoryObjects: { - en: "Objects", - args: undefined, + en: 'Objects', + args: undefined, }, emojiCategoryRecentlyUsed: { - en: "Recently Used", - args: undefined, + en: 'Recently Used', + args: undefined, }, emojiCategorySmileys: { - en: "Smileys and People", - args: undefined, + en: 'Smileys and People', + args: undefined, }, emojiCategorySymbols: { - en: "Symbols", - args: undefined, + en: 'Symbols', + args: undefined, }, emojiCategoryTravel: { - en: "Travel and Places", - args: undefined, + en: 'Travel and Places', + args: undefined, }, emojiReactsClearAll: { - en: "Are you sure you want to clear all {emoji}?", - args: {emoji: "string"} + en: 'Are you sure you want to clear all {emoji}?', + args: { emoji: 'string' }, }, emojiReactsCoolDown: { - en: "Slow down! You've sent too many emoji reacts. Try again soon", - args: undefined, + en: "Slow down! You've sent too many emoji reacts. Try again soon", + args: undefined, }, emojiReactsHoverNameDesktop: { - en: "{name} reacted with {emoji_name}", - args: {name: "string", emoji_name: "string"} + en: '{name} reacted with {emoji_name}', + args: { name: 'string', emoji_name: 'string' }, }, emojiReactsHoverNameTwoDesktop: { - en: "{name} and {other_name} reacted with {emoji_name}", - args: {name: "string", other_name: "string", emoji_name: "string"} + en: '{name} and {other_name} reacted with {emoji_name}', + args: { name: 'string', other_name: 'string', emoji_name: 'string' }, }, emojiReactsHoverTwoNameMultipleDesktop: { - en: "{name} and {count} others reacted with {emoji_name}", - args: {name: "string", count: "number", emoji_name: "string"} + en: '{name} and {count} others reacted with {emoji_name}', + args: { name: 'string', count: 'number', emoji_name: 'string' }, }, emojiReactsHoverYouNameDesktop: { - en: "You reacted with {emoji_name}", - args: {emoji_name: "string"} + en: 'You reacted with {emoji_name}', + args: { emoji_name: 'string' }, }, emojiReactsHoverYouNameMultipleDesktop: { - en: "You and {count} others reacted with {emoji_name}", - args: {count: "number", emoji_name: "string"} + en: 'You and {count} others reacted with {emoji_name}', + args: { count: 'number', emoji_name: 'string' }, }, emojiReactsHoverYouNameTwoDesktop: { - en: "You and {name} reacted with {emoji_name}", - args: {name: "string", emoji_name: "string"} + en: 'You and {name} reacted with {emoji_name}', + args: { name: 'string', emoji_name: 'string' }, }, emojiReactsNotification: { - en: "Reacted to your message {emoji}", - args: {emoji: "string"} + en: 'Reacted to your message {emoji}', + args: { emoji: 'string' }, }, enable: { - en: "Enable", - args: undefined, + en: 'Enable', + args: undefined, }, errorConnection: { - en: "Please check your internet connection and try again.", - args: undefined, + en: 'Please check your internet connection and try again.', + args: undefined, }, errorCopyAndQuit: { - en: "Copy Error and Quit", - args: undefined, + en: 'Copy Error and Quit', + args: undefined, }, errorDatabase: { - en: "Database Error", - args: undefined, + en: 'Database Error', + args: undefined, }, errorGeneric: { - en: "Something went wrong. Please try again later.", - args: undefined, + en: 'Something went wrong. Please try again later.', + args: undefined, }, errorUnknown: { - en: "An unknown error occurred.", - args: undefined, + en: 'An unknown error occurred.', + args: undefined, }, failedToDownload: { - en: "Failed to download", - args: undefined, + en: 'Failed to download', + args: undefined, }, failures: { - en: "Failures", - args: undefined, + en: 'Failures', + args: undefined, }, file: { - en: "File", - args: undefined, + en: 'File', + args: undefined, }, files: { - en: "Files", - args: undefined, + en: 'Files', + args: undefined, }, followSystemSettings: { - en: "Follow system settings", - args: undefined, + en: 'Follow system settings', + args: undefined, }, forever: { - en: "Forever", - args: undefined, + en: 'Forever', + args: undefined, }, from: { - en: "From:", - args: undefined, + en: 'From:', + args: undefined, }, fullScreenToggle: { - en: "Toggle Full Screen", - args: undefined, + en: 'Toggle Full Screen', + args: undefined, }, gif: { - en: "GIF", - args: undefined, + en: 'GIF', + args: undefined, }, giphyWarning: { - en: "Giphy", - args: undefined, + en: 'Giphy', + args: undefined, }, giphyWarningDescription: { - en: "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.", - args: undefined, + en: 'Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.', + args: undefined, }, groupAddMemberMaximum: { - en: "Groups have a maximum of 100 members", - args: undefined, + en: 'Groups have a maximum of 100 members', + args: undefined, }, groupCreate: { - en: "Create Group", - args: undefined, + en: 'Create Group', + args: undefined, }, groupCreateErrorNoMembers: { - en: "Please pick at least one other group member.", - args: undefined, + en: 'Please pick at least one other group member.', + args: undefined, }, groupDelete: { - en: "Delete Group", - args: undefined, + en: 'Delete Group', + args: undefined, }, groupDeleteDescription: { - en: "Are you sure you want to delete {group_name}?

This will remove all members and delete all group content.", - args: {group_name: "string"} + en: 'Are you sure you want to delete {group_name}?

This will remove all members and delete all group content.', + args: { group_name: 'string' }, }, groupDeleteDescriptionMember: { - en: "Are you sure you want to delete {group_name}?", - args: {group_name: "string"} + en: 'Are you sure you want to delete {group_name}?', + args: { group_name: 'string' }, }, groupDeletedMemberDescription: { - en: "{group_name} has been deleted by a group admin. You will not be able to send any more messages.", - args: {group_name: "string"} + en: '{group_name} has been deleted by a group admin. You will not be able to send any more messages.', + args: { group_name: 'string' }, }, groupDescriptionEnter: { - en: "Enter a group description", - args: undefined, + en: 'Enter a group description', + args: undefined, }, groupDisplayPictureUpdated: { - en: "Group display picture updated.", - args: undefined, + en: 'Group display picture updated.', + args: undefined, }, groupEdit: { - en: "Edit Group", - args: undefined, + en: 'Edit Group', + args: undefined, }, groupError: { - en: "Group Error", - args: undefined, + en: 'Group Error', + args: undefined, }, groupErrorCreate: { - en: "Failed to create group. Please check your internet connection and try again.", - args: undefined, + en: 'Failed to create group. Please check your internet connection and try again.', + args: undefined, }, groupErrorJoin: { - en: "Failed to join {group_name}", - args: {group_name: "string"} + en: 'Failed to join {group_name}', + args: { group_name: 'string' }, }, groupInformationSet: { - en: "Set Group Information", - args: undefined, + en: 'Set Group Information', + args: undefined, }, groupInviteDelete: { - en: "Are you sure you want to delete this group invite?", - args: undefined, + en: 'Are you sure you want to delete this group invite?', + args: undefined, }, groupInviteFailed: { - en: "Invite failed", - args: undefined, + en: 'Invite failed', + args: undefined, }, groupInviteFailedMultiple: { - en: "Failed to invite {name} and {count} others to {group_name}", - args: {name: "string", count: "number", group_name: "string"} + en: 'Failed to invite {name} and {count} others to {group_name}', + args: { name: 'string', count: 'number', group_name: 'string' }, }, groupInviteFailedTwo: { - en: "Failed to invite {name} and {other_name} to {group_name}", - args: {name: "string", other_name: "string", group_name: "string"} + en: 'Failed to invite {name} and {other_name} to {group_name}', + args: { name: 'string', other_name: 'string', group_name: 'string' }, }, groupInviteFailedUser: { - en: "Failed to invite {name} to {group_name}", - args: {name: "string", group_name: "string"} + en: 'Failed to invite {name} to {group_name}', + args: { name: 'string', group_name: 'string' }, }, groupInviteNotSent: { - en: "Invite not sent", - args: undefined, + en: 'Invite not sent', + args: undefined, }, groupInviteReinvite: { - en: "{name} invited you to rejoin {group_name}, where you are an Admin.", - args: {name: "string", group_name: "string"} + en: '{name} invited you to rejoin {group_name}, where you are an Admin.', + args: { name: 'string', group_name: 'string' }, }, groupInviteReinviteYou: { - en: "You were invited to rejoin {group_name}, where you are an Admin.", - args: {group_name: "string"} + en: 'You were invited to rejoin {group_name}, where you are an Admin.', + args: { group_name: 'string' }, }, groupInviteSent: { - en: "Invite sent", - args: undefined, + en: 'Invite sent', + args: undefined, }, groupInviteStatusUnknown: { - en: "Invite status unknown", - args: undefined, + en: 'Invite status unknown', + args: undefined, }, groupInviteSuccessful: { - en: "Group invite successful", - args: undefined, + en: 'Group invite successful', + args: undefined, }, groupInviteVersion: { - en: "Users must have the latest release to receive invitations", - args: undefined, + en: 'Users must have the latest release to receive invitations', + args: undefined, }, groupInviteYou: { - en: "You were invited to join the group.", - args: undefined, + en: 'You were invited to join the group.', + args: undefined, }, groupInviteYouAndMoreNew: { - en: "You and {count} others were invited to join the group.", - args: {count: "number"} + en: 'You and {count} others were invited to join the group.', + args: { count: 'number' }, }, groupInviteYouAndOtherNew: { - en: "You and {other_name} were invited to join the group.", - args: {other_name: "string"} + en: 'You and {other_name} were invited to join the group.', + args: { other_name: 'string' }, }, groupInviteYouHistory: { - en: "You were invited to join the group. Chat history was shared.", - args: undefined, + en: 'You were invited to join the group. Chat history was shared.', + args: undefined, }, groupLeave: { - en: "Leave Group", - args: undefined, + en: 'Leave Group', + args: undefined, }, groupLeaveDescription: { - en: "Are you sure you want to leave {group_name}?", - args: {group_name: "string"} + en: 'Are you sure you want to leave {group_name}?', + args: { group_name: 'string' }, }, groupLeaveDescriptionAdmin: { - en: "Are you sure you want to leave {group_name}?

This will remove all members and delete all group content.", - args: {group_name: "string"} + en: 'Are you sure you want to leave {group_name}?

This will remove all members and delete all group content.', + args: { group_name: 'string' }, }, groupLeaveErrorFailed: { - en: "Failed to leave {group_name}", - args: {group_name: "string"} + en: 'Failed to leave {group_name}', + args: { group_name: 'string' }, }, groupMemberLeft: { - en: "{name} left the group.", - args: {name: "string"} + en: '{name} left the group.', + args: { name: 'string' }, }, groupMemberLeftMultiple: { - en: "{name} and {count} others left the group.", - args: {name: "string", count: "number"} + en: '{name} and {count} others left the group.', + args: { name: 'string', count: 'number' }, }, groupMemberLeftTwo: { - en: "{name} and {other_name} left the group.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} left the group.', + args: { name: 'string', other_name: 'string' }, }, groupMemberNew: { - en: "{name} was invited to join the group.", - args: {name: "string"} + en: '{name} was invited to join the group.', + args: { name: 'string' }, }, groupMemberNewHistory: { - en: "{name} was invited to join the group. Chat history was shared.", - args: {name: "string"} + en: '{name} was invited to join the group. Chat history was shared.', + args: { name: 'string' }, }, groupMemberNewHistoryMultiple: { - en: "{name} and {count} others were invited to join the group. Chat history was shared.", - args: {name: "string", count: "number"} + en: '{name} and {count} others were invited to join the group. Chat history was shared.', + args: { name: 'string', count: 'number' }, }, groupMemberNewHistoryTwo: { - en: "{name} and {other_name} were invited to join the group. Chat history was shared.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} were invited to join the group. Chat history was shared.', + args: { name: 'string', other_name: 'string' }, }, groupMemberNewMultiple: { - en: "{name} and {count} others were invited to join the group.", - args: {name: "string", count: "number"} + en: '{name} and {count} others were invited to join the group.', + args: { name: 'string', count: 'number' }, }, groupMemberNewTwo: { - en: "{name} and {other_name} were invited to join the group.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} were invited to join the group.', + args: { name: 'string', other_name: 'string' }, }, groupMemberNewYouHistoryMultiple: { - en: "You and {count} others were invited to join the group. Chat history was shared.", - args: {count: "number"} + en: 'You and {count} others were invited to join the group. Chat history was shared.', + args: { count: 'number' }, }, groupMemberNewYouHistoryTwo: { - en: "You and {other_name} were invited to join the group. Chat history was shared.", - args: {other_name: "string"} + en: 'You and {other_name} were invited to join the group. Chat history was shared.', + args: { other_name: 'string' }, }, groupMemberYouLeft: { - en: "You left the group.", - args: undefined, + en: 'You left the group.', + args: undefined, }, groupMembers: { - en: "Group Members", - args: undefined, + en: 'Group Members', + args: undefined, }, groupMembersNone: { - en: "There are no other members in this group.", - args: undefined, + en: 'There are no other members in this group.', + args: undefined, }, groupName: { - en: "Group Name", - args: undefined, + en: 'Group Name', + args: undefined, }, groupNameEnter: { - en: "Enter a group name", - args: undefined, + en: 'Enter a group name', + args: undefined, }, groupNameEnterPlease: { - en: "Please enter a group name.", - args: undefined, + en: 'Please enter a group name.', + args: undefined, }, groupNameEnterShorter: { - en: "Please enter a shorter group name", - args: undefined, + en: 'Please enter a shorter group name', + args: undefined, }, groupNameNew: { - en: "Group name is now {group_name}.", - args: {group_name: "string"} + en: 'Group name is now {group_name}.', + args: { group_name: 'string' }, }, groupNameUpdated: { - en: "Group name updated.", - args: undefined, + en: 'Group name updated.', + args: undefined, }, groupNameVisible: { - en: "Group name is visible to all group members.", - args: undefined, + en: 'Group name is visible to all group members.', + args: undefined, }, groupNoMessages: { - en: "You have no messages from {group_name}. Send a message to start the conversation!", - args: {group_name: "string"} + en: 'You have no messages from {group_name}. Send a message to start the conversation!', + args: { group_name: 'string' }, }, groupNotUpdatedWarning: { - en: "This group has not been updated in over 30 days. You may experience issues sending messages or viewing group information.", - args: undefined, + en: 'This group has not been updated in over 30 days. You may experience issues sending messages or viewing group information.', + args: undefined, }, groupOnlyAdmin: { - en: "You are the only admin in {group_name}.

Group members and settings cannot be changed without an admin.", - args: {group_name: "string"} + en: 'You are the only admin in {group_name}.

Group members and settings cannot be changed without an admin.', + args: { group_name: 'string' }, }, groupPendingRemoval: { - en: "Pending removal", - args: undefined, + en: 'Pending removal', + args: undefined, }, groupPromotedYou: { - en: "You were promoted to Admin.", - args: undefined, + en: 'You were promoted to Admin.', + args: undefined, }, groupPromotedYouMultiple: { - en: "You and {count} others were promoted to Admin.", - args: {count: "number"} + en: 'You and {count} others were promoted to Admin.', + args: { count: 'number' }, }, groupPromotedYouTwo: { - en: "You and {other_name} were promoted to Admin.", - args: {other_name: "string"} + en: 'You and {other_name} were promoted to Admin.', + args: { other_name: 'string' }, }, groupRemoveDescription: { - en: "Would you like to remove {name} from {group_name}?", - args: {name: "string", group_name: "string"} + en: 'Would you like to remove {name} from {group_name}?', + args: { name: 'string', group_name: 'string' }, }, groupRemoveDescriptionMultiple: { - en: "Would you like to remove {name} and {count} others from {group_name}?", - args: {name: "string", count: "number", group_name: "string"} + en: 'Would you like to remove {name} and {count} others from {group_name}?', + args: { name: 'string', count: 'number', group_name: 'string' }, }, groupRemoveDescriptionTwo: { - en: "Would you like to remove {name} and {other_name} from {group_name}?", - args: {name: "string", other_name: "string", group_name: "string"} + en: 'Would you like to remove {name} and {other_name} from {group_name}?', + args: { name: 'string', other_name: 'string', group_name: 'string' }, }, groupRemoved: { - en: "{name} was removed from the group.", - args: {name: "string"} + en: '{name} was removed from the group.', + args: { name: 'string' }, }, groupRemovedMultiple: { - en: "{name} and {count} others were removed from the group.", - args: {name: "string", count: "number"} + en: '{name} and {count} others were removed from the group.', + args: { name: 'string', count: 'number' }, }, groupRemovedTwo: { - en: "{name} and {other_name} were removed from the group.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} were removed from the group.', + args: { name: 'string', other_name: 'string' }, }, groupRemovedYou: { - en: "You were removed from {group_name}.", - args: {group_name: "string"} + en: 'You were removed from {group_name}.', + args: { group_name: 'string' }, }, groupRemovedYouGeneral: { - en: "You were removed from the group.", - args: undefined, + en: 'You were removed from the group.', + args: undefined, }, groupRemovedYouMultiple: { - en: "You and {count} others were removed from the group.", - args: {count: "number"} + en: 'You and {count} others were removed from the group.', + args: { count: 'number' }, }, groupRemovedYouTwo: { - en: "You and {other_name} were removed from the group.", - args: {other_name: "string"} + en: 'You and {other_name} were removed from the group.', + args: { other_name: 'string' }, }, groupSetDisplayPicture: { - en: "Set Group Display Picture", - args: undefined, + en: 'Set Group Display Picture', + args: undefined, }, groupUnknown: { - en: "Unknown Group", - args: undefined, + en: 'Unknown Group', + args: undefined, }, groupUpdated: { - en: "Group updated", - args: undefined, + en: 'Group updated', + args: undefined, }, handlingConnectionCandidates: { - en: "Handling Connection Candidates", - args: undefined, + en: 'Handling Connection Candidates', + args: undefined, }, helpFAQ: { - en: "FAQ", - args: undefined, + en: 'FAQ', + args: undefined, }, helpHelpUsTranslateSession: { - en: "Help us translate Session", - args: undefined, + en: 'Help us translate Session', + args: undefined, }, helpReportABug: { - en: "Report a bug", - args: undefined, + en: 'Report a bug', + args: undefined, }, helpReportABugDescription: { - en: "Share some details to help us resolve your issue. Export your logs, then upload the file through Session's Help Desk.", - args: undefined, + en: "Share some details to help us resolve your issue. Export your logs, then upload the file through Session's Help Desk.", + args: undefined, }, helpReportABugExportLogs: { - en: "Export Logs", - args: undefined, + en: 'Export Logs', + args: undefined, }, helpReportABugExportLogsDescription: { - en: "Export your logs, then upload the file through Session's Help Desk.", - args: undefined, + en: "Export your logs, then upload the file through Session's Help Desk.", + args: undefined, }, helpReportABugExportLogsSaveToDesktop: { - en: "Save to desktop", - args: undefined, + en: 'Save to desktop', + args: undefined, }, helpReportABugExportLogsSaveToDesktopDescription: { - en: "Save this file to your desktop, then share it with Session developers.", - args: undefined, + en: 'Save this file to your desktop, then share it with Session developers.', + args: undefined, }, helpSupport: { - en: "Support", - args: undefined, + en: 'Support', + args: undefined, }, helpWedLoveYourFeedback: { - en: "We'd love your feedback", - args: undefined, + en: "We'd love your feedback", + args: undefined, }, hide: { - en: "Hide", - args: undefined, + en: 'Hide', + args: undefined, }, hideMenuBarDescription: { - en: "Toggle system menu bar visibility", - args: undefined, + en: 'Toggle system menu bar visibility', + args: undefined, }, hideNoteToSelfDescription: { - en: "Are you sure you want to hide Note to Self from your conversation list?", - args: undefined, + en: 'Are you sure you want to hide Note to Self from your conversation list?', + args: undefined, }, hideOthers: { - en: "Hide Others", - args: undefined, + en: 'Hide Others', + args: undefined, }, image: { - en: "Image", - args: undefined, + en: 'Image', + args: undefined, }, images: { - en: "images", - args: undefined, + en: 'images', + args: undefined, }, incognitoKeyboard: { - en: "Incognito Keyboard", - args: undefined, + en: 'Incognito Keyboard', + args: undefined, }, incognitoKeyboardDescription: { - en: "Request incognito mode if available. Depending on the keyboard you are using, your keyboard may ignore this request.", - args: undefined, + en: 'Request incognito mode if available. Depending on the keyboard you are using, your keyboard may ignore this request.', + args: undefined, }, info: { - en: "Info", - args: undefined, + en: 'Info', + args: undefined, }, invalidShortcut: { - en: "Invalid shortcut", - args: undefined, + en: 'Invalid shortcut', + args: undefined, }, join: { - en: "Join", - args: undefined, + en: 'Join', + args: undefined, }, later: { - en: "Later", - args: undefined, + en: 'Later', + args: undefined, }, learnMore: { - en: "Learn More", - args: undefined, + en: 'Learn More', + args: undefined, }, leave: { - en: "Leave", - args: undefined, + en: 'Leave', + args: undefined, }, leaving: { - en: "Leaving...", - args: undefined, + en: 'Leaving...', + args: undefined, }, legacyGroupAfterDeprecationAdmin: { - en: "This group is now read-only. Recreate this group to keep chatting.", - args: undefined, + en: 'This group is now read-only. Recreate this group to keep chatting.', + args: undefined, }, legacyGroupAfterDeprecationMember: { - en: "This group is now read-only. Ask the group admin to recreate this group to keep chatting.", - args: undefined, + en: 'This group is now read-only. Ask the group admin to recreate this group to keep chatting.', + args: undefined, }, legacyGroupBeforeDeprecationAdmin: { - en: "Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only at {date}.", - args: {date: "string"} + en: 'Groups have been upgraded! Recreate this group for improved reliability. This group will become read-only at {date}.', + args: { date: 'string' }, }, legacyGroupBeforeDeprecationMember: { - en: "Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only at {date}.", - args: {date: "string"} + en: 'Groups have been upgraded! Ask the group admin to recreate this group for improved reliability. This group will become read-only at {date}.', + args: { date: 'string' }, }, legacyGroupChatHistory: { - en: "Chat history will not be transferred to the new group. You can still view all chat history in your old group.", - args: undefined, + en: 'Chat history will not be transferred to the new group. You can still view all chat history in your old group.', + args: undefined, }, legacyGroupMemberNew: { - en: "{name} joined the group.", - args: {name: "string"} + en: '{name} joined the group.', + args: { name: 'string' }, }, legacyGroupMemberNewMultiple: { - en: "{name} and {count} others joined the group.", - args: {name: "string", count: "number"} + en: '{name} and {count} others joined the group.', + args: { name: 'string', count: 'number' }, }, legacyGroupMemberNewYouMultiple: { - en: "You and {count} others joined the group.", - args: {count: "number"} + en: 'You and {count} others joined the group.', + args: { count: 'number' }, }, legacyGroupMemberNewYouOther: { - en: "You and {other_name} joined the group.", - args: {other_name: "string"} + en: 'You and {other_name} joined the group.', + args: { other_name: 'string' }, }, legacyGroupMemberTwoNew: { - en: "{name} and {other_name} joined the group.", - args: {name: "string", other_name: "string"} + en: '{name} and {other_name} joined the group.', + args: { name: 'string', other_name: 'string' }, }, legacyGroupMemberYouNew: { - en: "You joined the group.", - args: undefined, + en: 'You joined the group.', + args: undefined, }, linkPreviews: { - en: "Link Previews", - args: undefined, + en: 'Link Previews', + args: undefined, }, linkPreviewsDescription: { - en: "Show link previews for supported URLs.", - args: undefined, + en: 'Show link previews for supported URLs.', + args: undefined, }, linkPreviewsEnable: { - en: "Enable Link Previews", - args: undefined, + en: 'Enable Link Previews', + args: undefined, }, linkPreviewsErrorLoad: { - en: "Unable to load link preview", - args: undefined, + en: 'Unable to load link preview', + args: undefined, }, linkPreviewsErrorUnsecure: { - en: "Preview not loaded for unsecure link", - args: undefined, + en: 'Preview not loaded for unsecure link', + args: undefined, }, linkPreviewsFirstDescription: { - en: "Display previews for URLs you send and receive. This can be useful, however Session must contact linked websites to generate previews. You can always turn off link previews in Session's settings.", - args: undefined, + en: "Display previews for URLs you send and receive. This can be useful, however Session must contact linked websites to generate previews. You can always turn off link previews in Session's settings.", + args: undefined, }, linkPreviewsSend: { - en: "Send Link Previews", - args: undefined, + en: 'Send Link Previews', + args: undefined, }, linkPreviewsSendModalDescription: { - en: "You will not have full metadata protection when sending link previews.", - args: undefined, + en: 'You will not have full metadata protection when sending link previews.', + args: undefined, }, linkPreviewsTurnedOff: { - en: "Link Previews Are Off", - args: undefined, + en: 'Link Previews Are Off', + args: undefined, }, 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.", - args: undefined, + en: "Session must contact linked websites to generate previews of links you send and receive.

You can turn them on in Session's settings.", + args: undefined, }, loadAccount: { - en: "Load Account", - args: undefined, + en: 'Load Account', + args: undefined, }, loadAccountProgressMessage: { - en: "Loading your account", - args: undefined, + en: 'Loading your account', + args: undefined, }, loading: { - en: "Loading...", - args: undefined, + en: 'Loading...', + args: undefined, }, lockApp: { - en: "Lock App", - args: undefined, + en: 'Lock App', + args: undefined, }, lockAppDescription: { - en: "Require fingerprint, PIN, pattern or password to unlock Session.", - args: undefined, + en: 'Require fingerprint, PIN, pattern or password to unlock Session.', + args: undefined, }, lockAppDescriptionIos: { - en: "Require Touch ID, Face ID or your passcode to unlock Session.", - args: undefined, + en: 'Require Touch ID, Face ID or your passcode to unlock Session.', + args: undefined, }, lockAppEnablePasscode: { - en: "You must enable a passcode in your iOS Settings in order to use Screen Lock.", - args: undefined, + en: 'You must enable a passcode in your iOS Settings in order to use Screen Lock.', + args: undefined, }, lockAppLocked: { - en: "Session is locked", - args: undefined, + en: 'Session is locked', + args: undefined, }, lockAppQuickResponse: { - en: "Quick response unavailable when Session is locked!", - args: undefined, + en: 'Quick response unavailable when Session is locked!', + args: undefined, }, lockAppStatus: { - en: "Lock status", - args: undefined, + en: 'Lock status', + args: undefined, }, lockAppUnlock: { - en: "Tap to unlock", - args: undefined, + en: 'Tap to unlock', + args: undefined, }, lockAppUnlocked: { - en: "Session is unlocked", - args: undefined, + en: 'Session is unlocked', + args: undefined, }, manageMembers: { - en: "Manage Members", - args: undefined, + en: 'Manage Members', + args: undefined, }, max: { - en: "Max", - args: undefined, + en: 'Max', + args: undefined, }, media: { - en: "Media", - args: undefined, + en: 'Media', + args: undefined, }, membersAddAccountIdOrOns: { - en: "Add Account ID or ONS", - args: undefined, + en: 'Add Account ID or ONS', + args: undefined, }, membersInvite: { - en: "Invite Contacts", - args: undefined, + en: 'Invite Contacts', + args: undefined, }, membersInviteShareDescription: { - en: "Would you like to share group message history with {name}?", - args: {name: "string"} + en: 'Would you like to share group message history with {name}?', + args: { name: 'string' }, }, membersInviteShareDescriptionMultiple: { - en: "Would you like to share group message history with {name} and {count} others?", - args: {name: "string", count: "number"} + en: 'Would you like to share group message history with {name} and {count} others?', + args: { name: 'string', count: 'number' }, }, membersInviteShareDescriptionTwo: { - en: "Would you like to share group message history with {name} and {other_name}?", - args: {name: "string", other_name: "string"} + en: 'Would you like to share group message history with {name} and {other_name}?', + args: { name: 'string', other_name: 'string' }, }, membersInviteShareMessageHistory: { - en: "Share message history", - args: undefined, + en: 'Share message history', + args: undefined, }, membersInviteShareNewMessagesOnly: { - en: "Share new messages only", - args: undefined, + en: 'Share new messages only', + args: undefined, }, membersInviteTitle: { - en: "Invite", - args: undefined, + en: 'Invite', + args: undefined, }, message: { - en: "Message", - args: undefined, + en: 'Message', + args: undefined, }, messageBubbleReadMore: { - en: "Read more", - args: undefined, + en: 'Read more', + args: undefined, }, messageEmpty: { - en: "This message is empty.", - args: undefined, + en: 'This message is empty.', + args: undefined, }, messageErrorDelivery: { - en: "Message delivery failed", - args: undefined, + en: 'Message delivery failed', + args: undefined, }, messageErrorLimit: { - en: "Message limit reached", - args: undefined, + en: 'Message limit reached', + args: undefined, }, messageErrorOld: { - en: "Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.", - args: undefined, + en: 'Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.', + args: undefined, }, messageErrorOriginal: { - en: "Original message not found", - args: undefined, + en: 'Original message not found', + args: undefined, }, messageInfo: { - en: "Message Info", - args: undefined, + en: 'Message Info', + args: undefined, }, messageMarkRead: { - en: "Mark read", - args: undefined, + en: 'Mark read', + args: undefined, }, messageMarkUnread: { - en: "Mark unread", - args: undefined, + en: 'Mark unread', + args: undefined, }, messageNewDescriptionDesktop: { - en: "Start a new conversation by entering your friend's Account ID or ONS.", - args: undefined, + en: "Start a new conversation by entering your friend's Account ID or ONS.", + args: undefined, }, messageNewDescriptionMobile: { - en: "Start a new conversation by entering your friend's Account ID, ONS or scanning their QR code.", - args: undefined, + en: "Start a new conversation by entering your friend's Account ID, ONS or scanning their QR code.", + args: undefined, }, messageReplyingTo: { - en: "Replying to", - args: undefined, + en: 'Replying to', + args: undefined, }, messageRequestDisabledToastAttachments: { - en: "You cannot send attachments until your Message Request is accepted", - args: undefined, + en: 'You cannot send attachments until your Message Request is accepted', + args: undefined, }, messageRequestDisabledToastVoiceMessages: { - en: "You cannot send voice messages until your Message Request is accepted", - args: undefined, + en: 'You cannot send voice messages until your Message Request is accepted', + args: undefined, }, messageRequestGroupInvite: { - en: "{name} invited you to join {group_name}.", - args: {name: "string", group_name: "string"} + en: '{name} invited you to join {group_name}.', + args: { name: 'string', group_name: 'string' }, }, messageRequestGroupInviteDescription: { - en: "Sending a message to this group will automatically accept the group invite.", - args: undefined, + en: 'Sending a message to this group will automatically accept the group invite.', + args: undefined, }, messageRequestPending: { - en: "Your message request is currently pending.", - args: undefined, + en: 'Your message request is currently pending.', + args: undefined, }, messageRequestPendingDescription: { - en: "You will be able to send voice messages and attachments once the recipient has approved this message request.", - args: undefined, + en: 'You will be able to send voice messages and attachments once the recipient has approved this message request.', + args: undefined, }, messageRequestYouHaveAccepted: { - en: "You have accepted the message request from {name}.", - args: {name: "string"} + en: 'You have accepted the message request from {name}.', + args: { name: 'string' }, }, messageRequestsAcceptDescription: { - en: "Sending a message to this user will automatically accept their message request and reveal your Account ID.", - args: undefined, + en: 'Sending a message to this user will automatically accept their message request and reveal your Account ID.', + args: undefined, }, messageRequestsAccepted: { - en: "Your message request has been accepted.", - args: undefined, + en: 'Your message request has been accepted.', + args: undefined, }, messageRequestsClearAllExplanation: { - en: "Are you sure you want to clear all message requests and group invites?", - args: undefined, + en: 'Are you sure you want to clear all message requests and group invites?', + args: undefined, }, messageRequestsCommunities: { - en: "Community Message Requests", - args: undefined, + en: 'Community Message Requests', + args: undefined, }, messageRequestsCommunitiesDescription: { - en: "Allow message requests from Community conversations.", - args: undefined, + en: 'Allow message requests from Community conversations.', + args: undefined, }, messageRequestsContactDelete: { - en: "Are you sure you want to delete this message request and the associated contact?", - args: undefined, + en: 'Are you sure you want to delete this message request and the associated contact?', + args: undefined, }, messageRequestsDelete: { - en: "Are you sure you want to delete this message request?", - args: undefined, + en: 'Are you sure you want to delete this message request?', + args: undefined, }, messageRequestsNew: { - en: "You have a new message request", - args: undefined, + en: 'You have a new message request', + args: undefined, }, messageRequestsNonePending: { - en: "No pending message requests", - args: undefined, + en: 'No pending message requests', + args: undefined, }, messageRequestsTurnedOff: { - en: "{name} has message requests from Community conversations turned off, so you cannot send them a message.", - args: {name: "string"} + en: '{name} has message requests from Community conversations turned off, so you cannot send them a message.', + args: { name: 'string' }, }, messageSelect: { - en: "Select Message", - args: undefined, + en: 'Select Message', + args: undefined, }, messageSnippetGroup: { - en: "{author}: {message_snippet}", - args: {author: "string", message_snippet: "string"} + en: '{author}: {message_snippet}', + args: { author: 'string', message_snippet: 'string' }, }, messageStatusFailedToSend: { - en: "Failed to send", - args: undefined, + en: 'Failed to send', + args: undefined, }, messageStatusFailedToSync: { - en: "Failed to sync", - args: undefined, + en: 'Failed to sync', + args: undefined, }, messageStatusSyncing: { - en: "Syncing", - args: undefined, + en: 'Syncing', + args: undefined, }, messageUnread: { - en: "Unread messages", - args: undefined, + en: 'Unread messages', + args: undefined, }, messageVoice: { - en: "Voice Message", - args: undefined, + en: 'Voice Message', + args: undefined, }, messageVoiceErrorShort: { - en: "Hold to record a voice message", - args: undefined, + en: 'Hold to record a voice message', + args: undefined, }, messageVoiceSlideToCancel: { - en: "Slide to Cancel", - args: undefined, + en: 'Slide to Cancel', + args: undefined, }, messageVoiceSnippet: { - en: "{emoji} Voice Message", - args: {emoji: "string"} + en: '{emoji} Voice Message', + args: { emoji: 'string' }, }, messageVoiceSnippetGroup: { - en: "{author}: {emoji} Voice Message", - args: {author: "string", emoji: "string"} + en: '{author}: {emoji} Voice Message', + args: { author: 'string', emoji: 'string' }, }, messages: { - en: "Messages", - args: undefined, + en: 'Messages', + args: undefined, }, minimize: { - en: "Minimize", - args: undefined, + en: 'Minimize', + args: undefined, }, modalMessageCharacterDisplayTitle: { - en: "Message Length", - args: undefined, + en: 'Message Length', + args: undefined, }, modalMessageCharacterTooLongDescription: { - en: "You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less.", - args: {limit: "string"} + en: 'You have exceeded the character limit for this message. Please shorten your message to {limit} characters or less.', + args: { limit: 'string' }, }, modalMessageCharacterTooLongTitle: { - en: "Message Too Long", - args: undefined, + en: 'Message Too Long', + args: undefined, }, modalMessageTooLongDescription: { - en: "Please shorten your message to {limit} characters or less.", - args: {limit: "string"} + en: 'Please shorten your message to {limit} characters or less.', + args: { limit: 'string' }, }, modalMessageTooLongTitle: { - en: "Message Too Long", - args: undefined, + en: 'Message Too Long', + args: undefined, }, next: { - en: "Next", - args: undefined, + en: 'Next', + args: undefined, }, nicknameDescription: { - en: "Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations.", - args: {name: "string"} + en: 'Choose a nickname for {name}. This will appear to you in your one-to-one and group conversations.', + args: { name: 'string' }, }, nicknameEnter: { - en: "Enter nickname", - args: undefined, + en: 'Enter nickname', + args: undefined, }, nicknameErrorShorter: { - en: "Please enter a shorter nickname", - args: undefined, + en: 'Please enter a shorter nickname', + args: undefined, }, nicknameRemove: { - en: "Remove Nickname", - args: undefined, + en: 'Remove Nickname', + args: undefined, }, nicknameSet: { - en: "Set Nickname", - args: undefined, + en: 'Set Nickname', + args: undefined, }, no: { - en: "No", - args: undefined, + en: 'No', + args: undefined, }, noSuggestions: { - en: "No Suggestions", - args: undefined, + en: 'No Suggestions', + args: undefined, }, none: { - en: "None", - args: undefined, + en: 'None', + args: undefined, }, notNow: { - en: "Not now", - args: undefined, + en: 'Not now', + args: undefined, }, noteToSelf: { - en: "Note to Self", - args: undefined, + en: 'Note to Self', + args: undefined, }, noteToSelfEmpty: { - en: "You have no messages in Note to Self.", - args: undefined, + en: 'You have no messages in Note to Self.', + args: undefined, }, noteToSelfHide: { - en: "Hide Note to Self", - args: undefined, + en: 'Hide Note to Self', + args: undefined, }, noteToSelfHideDescription: { - en: "Are you sure you want to hide Note to Self?", - args: undefined, + en: 'Are you sure you want to hide Note to Self?', + args: undefined, }, notificationsAllMessages: { - en: "All Messages", - args: undefined, + en: 'All Messages', + args: undefined, }, notificationsContent: { - en: "Notification Content", - args: undefined, + en: 'Notification Content', + args: undefined, }, notificationsContentDescription: { - en: "The information shown in notifications.", - args: undefined, + en: 'The information shown in notifications.', + args: undefined, }, notificationsContentShowNameAndContent: { - en: "Name and Content", - args: undefined, + en: 'Name and Content', + args: undefined, }, notificationsContentShowNameOnly: { - en: "Name Only", - args: undefined, + en: 'Name Only', + args: undefined, }, notificationsContentShowNoNameOrContent: { - en: "No Name or Content", - args: undefined, + en: 'No Name or Content', + args: undefined, }, notificationsFastMode: { - en: "Fast Mode", - args: undefined, + en: 'Fast Mode', + args: undefined, }, notificationsFastModeDescription: { - en: "You'll be notified of new messages reliably and immediately using Google's notification Servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Google's notification Servers.", + args: undefined, }, notificationsFastModeDescriptionHuawei: { - en: "You'll be notified of new messages reliably and immediately using Huawei’s notification servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Huawei’s notification servers.", + args: undefined, }, notificationsFastModeDescriptionIos: { - en: "You'll be notified of new messages reliably and immediately using Apple's notification Servers.", - args: undefined, + en: "You'll be notified of new messages reliably and immediately using Apple's notification Servers.", + args: undefined, }, notificationsGoToDevice: { - en: "Go to device notification settings", - args: undefined, + en: 'Go to device notification settings', + args: undefined, }, notificationsHeaderAllMessages: { - en: "Notifications - All", - args: undefined, + en: 'Notifications - All', + args: undefined, }, notificationsHeaderMentionsOnly: { - en: "Notifications - Mentions Only", - args: undefined, + en: 'Notifications - Mentions Only', + args: undefined, }, notificationsHeaderMute: { - en: "Notifications - Muted", - args: undefined, + en: 'Notifications - Muted', + args: undefined, }, notificationsIosGroup: { - en: "{name} to {conversation_name}", - args: {name: "string", conversation_name: "string"} + en: '{name} to {conversation_name}', + args: { name: 'string', conversation_name: 'string' }, }, notificationsIosRestart: { - en: "You may have received messages while your {device} was restarting.", - args: {device: "string"} + en: 'You may have received messages while your {device} was restarting.', + args: { device: 'string' }, }, notificationsLedColor: { - en: "LED color", - args: undefined, + en: 'LED color', + args: undefined, }, notificationsMentionsOnly: { - en: "Mentions Only", - args: undefined, + en: 'Mentions Only', + args: undefined, }, notificationsMessage: { - en: "Message notifications", - args: undefined, + en: 'Message notifications', + args: undefined, }, notificationsMostRecent: { - en: "Most recent from {name}", - args: {name: "string"} + en: 'Most recent from {name}', + args: { name: 'string' }, }, notificationsMute: { - en: "Mute", - args: undefined, + en: 'Mute', + args: undefined, }, notificationsMuteFor: { - en: "Mute for {time_large}", - args: {time_large: "string"} + en: 'Mute for {time_large}', + args: { time_large: 'string' }, }, notificationsMuteUnmute: { - en: "Unmute", - args: undefined, + en: 'Unmute', + args: undefined, }, notificationsMuted: { - en: "Muted", - args: undefined, + en: 'Muted', + args: undefined, }, notificationsMutedFor: { - en: "Muted for {time_large}", - args: {time_large: "string"} + en: 'Muted for {time_large}', + args: { time_large: 'string' }, }, notificationsMutedForTime: { - en: "Muted until {date_time}", - args: {date_time: "string"} + en: 'Muted until {date_time}', + args: { date_time: 'string' }, }, notificationsSlowMode: { - en: "Slow Mode", - args: undefined, + en: 'Slow Mode', + args: undefined, }, notificationsSlowModeDescription: { - en: "Session will occasionally check for new messages in the background.", - args: undefined, + en: 'Session will occasionally check for new messages in the background.', + args: undefined, }, notificationsSound: { - en: "Sound", - args: undefined, + en: 'Sound', + args: undefined, }, notificationsSoundDescription: { - en: "Sound when App is open", - args: undefined, + en: 'Sound when App is open', + args: undefined, }, notificationsSoundDesktop: { - en: "Audio Notifications", - args: undefined, + en: 'Audio Notifications', + args: undefined, }, notificationsStrategy: { - en: "Notification Strategy", - args: undefined, + en: 'Notification Strategy', + args: undefined, }, notificationsStyle: { - en: "Notification Style", - args: undefined, + en: 'Notification Style', + args: undefined, }, notificationsSystem: { - en: "{message_count} new messages in {conversation_count} conversations", - args: {message_count: "string", conversation_count: "string"} + en: '{message_count} new messages in {conversation_count} conversations', + args: { message_count: 'string', conversation_count: 'string' }, }, notificationsVibrate: { - en: "Vibrate", - args: undefined, + en: 'Vibrate', + args: undefined, }, off: { - en: "Off", - args: undefined, + en: 'Off', + args: undefined, }, okay: { - en: "Okay", - args: undefined, + en: 'Okay', + args: undefined, }, on: { - en: "On", - args: undefined, + en: 'On', + args: undefined, }, onboardingAccountCreate: { - en: "Create account", - args: undefined, + en: 'Create account', + args: undefined, }, onboardingAccountCreated: { - en: "Account Created", - args: undefined, + en: 'Account Created', + args: undefined, }, onboardingAccountExists: { - en: "I have an account", - args: undefined, + en: 'I have an account', + args: undefined, }, onboardingBackAccountCreation: { - en: "You cannot go back further. In order to cancel your account creation, Session needs to quit.", - args: undefined, + en: 'You cannot go back further. In order to cancel your account creation, Session needs to quit.', + args: undefined, }, onboardingBackLoadAccount: { - en: "You cannot go back further. In order to stop loading your account, Session needs to quit.", - args: undefined, + en: 'You cannot go back further. In order to stop loading your account, Session needs to quit.', + args: undefined, }, onboardingBubbleCreatingAnAccountIsEasy: { - en: "Creating an account is instant, free, and anonymous {emoji}", - args: {emoji: "string"} + en: 'Creating an account is instant, free, and anonymous {emoji}', + args: { emoji: 'string' }, }, onboardingBubbleNoPhoneNumber: { - en: "You don't even need a phone number to sign up.", - args: undefined, + en: "You don't even need a phone number to sign up.", + args: undefined, }, onboardingBubblePrivacyInYourPocket: { - en: "Privacy in your pocket.", - args: undefined, + en: 'Privacy in your pocket.', + args: undefined, }, onboardingBubbleSessionIsEngineered: { - en: "Session is engineered to protect your privacy.", - args: undefined, + en: 'Session is engineered to protect your privacy.', + args: undefined, }, onboardingBubbleWelcomeToSession: { - en: "Welcome to Session {emoji}", - args: {emoji: "string"} + en: 'Welcome to Session {emoji}', + args: { emoji: 'string' }, }, onboardingHitThePlusButton: { - en: "Hit the plus button to start a chat, create a group, or join an official community!", - args: undefined, + en: 'Hit the plus button to start a chat, create a group, or join an official community!', + args: undefined, }, onboardingMessageNotificationExplanation: { - en: "There are two ways Session can notify you of new messages.", - args: undefined, + en: 'There are two ways Session can notify you of new messages.', + args: undefined, }, onboardingPrivacy: { - en: "Privacy Policy", - args: undefined, + en: 'Privacy Policy', + args: undefined, }, onboardingTos: { - en: "Terms of Service", - args: undefined, + en: 'Terms of Service', + args: undefined, }, onboardingTosPrivacy: { - en: "By using this service, you agree to our Terms of Service and Privacy Policy", - args: undefined, + en: 'By using this service, you agree to our Terms of Service and Privacy Policy', + args: undefined, }, onionRoutingPath: { - en: "Path", - args: undefined, + en: 'Path', + args: undefined, }, onionRoutingPathDescription: { - en: "Session hides your IP by routing your messages through multiple service nodes in Session's decentralized network. This is your current path:", - args: undefined, + en: "Session hides your IP by routing your messages through multiple service nodes in Session's decentralized network. This is your current path:", + args: undefined, }, onionRoutingPathDestination: { - en: "Destination", - args: undefined, + en: 'Destination', + args: undefined, }, onionRoutingPathEntryNode: { - en: "Entry Node", - args: undefined, + en: 'Entry Node', + args: undefined, }, onionRoutingPathServiceNode: { - en: "Service Node", - args: undefined, + en: 'Service Node', + args: undefined, }, onionRoutingPathUnknownCountry: { - en: "Unknown Country", - args: undefined, + en: 'Unknown Country', + args: undefined, }, onsErrorNotRecognized: { - en: "We couldn't recognize this ONS. Please check it and try again.", - args: undefined, + en: "We couldn't recognize this ONS. Please check it and try again.", + args: undefined, }, onsErrorUnableToSearch: { - en: "We were unable to search for this ONS. Please try again later.", - args: undefined, + en: 'We were unable to search for this ONS. Please try again later.', + args: undefined, }, open: { - en: "Open", - args: undefined, + en: 'Open', + args: undefined, }, other: { - en: "Other", - args: undefined, + en: 'Other', + args: undefined, }, passwordChange: { - en: "Change Password", - args: undefined, + en: 'Change Password', + args: undefined, }, passwordChangeDescription: { - en: "Change the password required to unlock Session.", - args: undefined, + en: 'Change the password required to unlock Session.', + args: undefined, }, passwordChangedDescription: { - en: "Your password has been changed. Please keep it safe.", - args: undefined, + en: 'Your password has been changed. Please keep it safe.', + args: undefined, }, passwordConfirm: { - en: "Confirm password", - args: undefined, + en: 'Confirm password', + args: undefined, }, passwordCreate: { - en: "Create your password", - args: undefined, + en: 'Create your password', + args: undefined, }, passwordCurrentIncorrect: { - en: "Your current password is incorrect.", - args: undefined, + en: 'Your current password is incorrect.', + args: undefined, }, passwordDescription: { - en: "Require password to unlock Session.", - args: undefined, + en: 'Require password to unlock Session.', + args: undefined, }, passwordEnter: { - en: "Enter password", - args: undefined, + en: 'Enter password', + args: undefined, }, passwordEnterCurrent: { - en: "Please enter your current password", - args: undefined, + en: 'Please enter your current password', + args: undefined, }, passwordEnterNew: { - en: "Please enter your new password", - args: undefined, + en: 'Please enter your new password', + args: undefined, }, passwordError: { - en: "Password must only contain letters, numbers and symbols", - args: undefined, + en: 'Password must only contain letters, numbers and symbols', + args: undefined, }, passwordErrorLength: { - en: "Password must be between 6 and 64 characters long", - args: undefined, + en: 'Password must be between 6 and 64 characters long', + args: undefined, }, passwordErrorMatch: { - en: "Passwords do not match", - args: undefined, + en: 'Passwords do not match', + args: undefined, }, passwordFailed: { - en: "Failed to set password", - args: undefined, + en: 'Failed to set password', + args: undefined, }, passwordIncorrect: { - en: "Incorrect password", - args: undefined, + en: 'Incorrect password', + args: undefined, }, passwordRemove: { - en: "Remove Password", - args: undefined, + en: 'Remove Password', + args: undefined, }, passwordRemoveDescription: { - en: "Remove the password required to unlock Session.", - args: undefined, + en: 'Remove the password required to unlock Session.', + args: undefined, }, passwordRemovedDescription: { - en: "Your password has been removed.", - args: undefined, + en: 'Your password has been removed.', + args: undefined, }, passwordSet: { - en: "Set Password", - args: undefined, + en: 'Set Password', + args: undefined, }, passwordSetDescription: { - en: "Your password has been set. Please keep it safe.", - args: undefined, + en: 'Your password has been set. Please keep it safe.', + args: undefined, }, paste: { - en: "Paste", - args: undefined, + en: 'Paste', + args: undefined, }, permissionChange: { - en: "Permission Change", - args: undefined, + en: 'Permission Change', + args: undefined, }, permissionMusicAudioDenied: { - en: "Session needs music and audio access in order to send files, music and audio, but it has been permanently denied. Tap Settings → Permissions, and turn \"Music and audio\" on.", - args: undefined, + en: 'Session needs music and audio access in order to send files, music and audio, but it has been permanently denied. Tap Settings → Permissions, and turn "Music and audio" on.', + args: undefined, }, permissionsAppleMusic: { - en: "Session needs to use Apple Music to play media attachments.", - args: undefined, + en: 'Session needs to use Apple Music to play media attachments.', + args: undefined, }, permissionsAutoUpdate: { - en: "Auto Update", - args: undefined, + en: 'Auto Update', + args: undefined, }, permissionsAutoUpdateDescription: { - en: "Automatically check for updates on startup.", - args: undefined, + en: 'Automatically check for updates on startup.', + args: undefined, }, permissionsCameraAccessRequiredCallsIos: { - en: "Camera access is required to make video calls. Toggle the \"Camera\" permission in Settings to continue.", - args: undefined, + en: 'Camera access is required to make video calls. Toggle the "Camera" permission in Settings to continue.', + args: undefined, }, permissionsCameraChangeDescriptionIos: { - en: "Camera access is currently enabled. To disable it, toggle the \"Camera\" permission in Settings.", - args: undefined, + en: 'Camera access is currently enabled. To disable it, toggle the "Camera" permission in Settings.', + args: undefined, }, permissionsCameraDenied: { - en: "Session needs camera access to take photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn \"Camera\" on.", - args: undefined, + en: 'Session needs camera access to take photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn "Camera" on.', + args: undefined, }, permissionsCameraDescriptionIos: { - en: "Allow access to camera for video calls.", - args: undefined, + en: 'Allow access to camera for video calls.', + args: undefined, }, permissionsFaceId: { - en: "The screen lock feature on Session uses Face ID.", - args: undefined, + en: 'The screen lock feature on Session uses Face ID.', + args: undefined, }, permissionsKeepInSystemTray: { - en: "Keep in System Tray", - args: undefined, + en: 'Keep in System Tray', + args: undefined, }, permissionsKeepInSystemTrayDescription: { - en: "Session continues running in the background when you close the window", - args: undefined, + en: 'Session continues running in the background when you close the window', + args: undefined, }, permissionsLibrary: { - en: "Session needs photo library access to continue. You can enable access in the iOS settings.", - args: undefined, + en: 'Session needs photo library access to continue. You can enable access in the iOS settings.', + args: undefined, }, permissionsLocalNetworkAccessRequiredCallsIos: { - en: "Local Network access is required to facilitate calls. Toggle the \"Local Network\" permission in Settings to continue.", - args: undefined, + en: 'Local Network access is required to facilitate calls. Toggle the "Local Network" permission in Settings to continue.', + args: undefined, }, permissionsLocalNetworkAccessRequiredIos: { - en: "Session needs access to local network to make voice and video calls.", - args: undefined, + en: 'Session needs access to local network to make voice and video calls.', + args: undefined, }, permissionsLocalNetworkChangeDescriptionIos: { - en: "Local Network access is currently enabled. To disable it, toggle the \"Local Network\" permission in Settings.", - args: undefined, + en: 'Local Network access is currently enabled. To disable it, toggle the "Local Network" permission in Settings.', + args: undefined, }, permissionsLocalNetworkDescriptionIos: { - en: "Allow access to local network to facilitate voice and video calls.", - args: undefined, + en: 'Allow access to local network to facilitate voice and video calls.', + args: undefined, }, permissionsLocalNetworkIos: { - en: "Local Network", - args: undefined, + en: 'Local Network', + args: undefined, }, permissionsMicrophone: { - en: "Microphone", - args: undefined, + en: 'Microphone', + args: undefined, }, permissionsMicrophoneAccessRequired: { - en: "Session needs microphone access to make calls and send audio messages, but it has been permanently denied. Tap settings → Permissions, and turn \"Microphone\" on.", - args: undefined, + en: 'Session needs microphone access to make calls and send audio messages, but it has been permanently denied. Tap settings → Permissions, and turn "Microphone" on.', + args: undefined, }, permissionsMicrophoneAccessRequiredCallsIos: { - en: "Microphone access is required to make calls and record audio messages. Toggle the \"Microphone\" permission in Settings to continue.", - args: undefined, + en: 'Microphone access is required to make calls and record audio messages. Toggle the "Microphone" permission in Settings to continue.', + args: undefined, }, permissionsMicrophoneAccessRequiredDesktop: { - en: "You can enable microphone access in Session's privacy settings", - args: undefined, + en: "You can enable microphone access in Session's privacy settings", + args: undefined, }, permissionsMicrophoneAccessRequiredIos: { - en: "Session needs microphone access to make calls and record audio messages.", - args: undefined, + en: 'Session needs microphone access to make calls and record audio messages.', + args: undefined, }, permissionsMicrophoneChangeDescriptionIos: { - en: "Microphone access is currently enabled. To disable it, toggle the \"Microphone\" permission in Settings.", - args: undefined, + en: 'Microphone access is currently enabled. To disable it, toggle the "Microphone" permission in Settings.', + args: undefined, }, permissionsMicrophoneDescription: { - en: "Allow access to microphone.", - args: undefined, + en: 'Allow access to microphone.', + args: undefined, }, permissionsMicrophoneDescriptionIos: { - en: "Allow access to microphone for voice calls and audio messages.", - args: undefined, + en: 'Allow access to microphone for voice calls and audio messages.', + args: undefined, }, permissionsMusicAudio: { - en: "Session needs music and audio access in order to send files, music and audio.", - args: undefined, + en: 'Session needs music and audio access in order to send files, music and audio.', + args: undefined, }, permissionsRequired: { - en: "Permission Required", - args: undefined, + en: 'Permission Required', + args: undefined, }, permissionsStorageDenied: { - en: "Session needs photo library access so you can send photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn \"Photos and videos\" on.", - args: undefined, + en: 'Session needs photo library access so you can send photos and videos, but it has been permanently denied. Tap Settings → Permissions, and turn "Photos and videos" on.', + args: undefined, }, permissionsStorageDeniedLegacy: { - en: "Session needs storage access so you can send and save attachments. Tap Settings → Permissions, and turn \"Storage\" on.", - args: undefined, + en: 'Session needs storage access so you can send and save attachments. Tap Settings → Permissions, and turn "Storage" on.', + args: undefined, }, permissionsStorageSave: { - en: "Session needs storage access to save attachments and media.", - args: undefined, + en: 'Session needs storage access to save attachments and media.', + args: undefined, }, permissionsStorageSaveDenied: { - en: "Session needs storage access to save photos and videos, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Storage\".", - args: undefined, + en: 'Session needs storage access to save photos and videos, but it has been permanently denied. Please continue to app settings, select "Permissions", and enable "Storage".', + args: undefined, }, permissionsStorageSend: { - en: "Session needs storage access to send photos and videos.", - args: undefined, + en: 'Session needs storage access to send photos and videos.', + args: undefined, }, permissionsWriteCommunity: { - en: "You don't have write permissions in this community", - args: undefined, + en: "You don't have write permissions in this community", + args: undefined, }, pin: { - en: "Pin", - args: undefined, + en: 'Pin', + args: undefined, }, pinConversation: { - en: "Pin Conversation", - args: undefined, + en: 'Pin Conversation', + args: undefined, }, pinUnpin: { - en: "Unpin", - args: undefined, + en: 'Unpin', + args: undefined, }, pinUnpinConversation: { - en: "Unpin Conversation", - args: undefined, + en: 'Unpin Conversation', + args: undefined, }, preview: { - en: "Preview", - args: undefined, + en: 'Preview', + args: undefined, }, profile: { - en: "Profile", - args: undefined, + en: 'Profile', + args: undefined, }, profileDisplayPicture: { - en: "Display Picture", - args: undefined, + en: 'Display Picture', + args: undefined, }, profileDisplayPictureRemoveError: { - en: "Failed to remove display picture.", - args: undefined, + en: 'Failed to remove display picture.', + args: undefined, }, profileDisplayPictureSet: { - en: "Set Display Picture", - args: undefined, + en: 'Set Display Picture', + args: undefined, }, profileDisplayPictureSizeError: { - en: "Please pick a smaller file.", - args: undefined, + en: 'Please pick a smaller file.', + args: undefined, }, profileErrorUpdate: { - en: "Failed to update profile.", - args: undefined, + en: 'Failed to update profile.', + args: undefined, }, promote: { - en: "Promote", - args: undefined, + en: 'Promote', + args: undefined, }, qrCode: { - en: "QR Code", - args: undefined, + en: 'QR Code', + args: undefined, }, qrNotAccountId: { - en: "This QR code does not contain an Account ID", - args: undefined, + en: 'This QR code does not contain an Account ID', + args: undefined, }, qrNotRecoveryPassword: { - en: "This QR code does not contain a Recovery Password", - args: undefined, + en: 'This QR code does not contain a Recovery Password', + args: undefined, }, qrScan: { - en: "Scan QR Code", - args: undefined, + en: 'Scan QR Code', + args: undefined, }, qrView: { - en: "View QR", - args: undefined, + en: 'View QR', + args: undefined, }, qrYoursDescription: { - en: "Friends can message you by scanning your QR code.", - args: undefined, + en: 'Friends can message you by scanning your QR code.', + args: undefined, }, quit: { - en: "Quit Session", - args: undefined, + en: 'Quit Session', + args: undefined, }, quitButton: { - en: "Quit", - args: undefined, + en: 'Quit', + args: undefined, }, read: { - en: "Read", - args: undefined, + en: 'Read', + args: undefined, }, readReceipts: { - en: "Read Receipts", - args: undefined, + en: 'Read Receipts', + args: undefined, }, readReceiptsDescription: { - en: "Show read receipts for all messages you send and receive.", - args: undefined, + en: 'Show read receipts for all messages you send and receive.', + args: undefined, }, received: { - en: "Received:", - args: undefined, + en: 'Received:', + args: undefined, }, receivedAnswer: { - en: "Received Answer", - args: undefined, + en: 'Received Answer', + args: undefined, }, receivingCallOffer: { - en: "Receiving Call Offer", - args: undefined, + en: 'Receiving Call Offer', + args: undefined, }, receivingPreOffer: { - en: "Receiving Pre Offer", - args: undefined, + en: 'Receiving Pre Offer', + args: undefined, }, recommended: { - en: "Recommended", - args: undefined, + en: 'Recommended', + args: undefined, }, recoveryPasswordBannerDescription: { - en: "Save your recovery password to make sure you don't lose access to your account.", - args: undefined, + en: "Save your recovery password to make sure you don't lose access to your account.", + args: undefined, }, recoveryPasswordBannerTitle: { - en: "Save your recovery password", - args: undefined, + en: 'Save your recovery password', + args: undefined, }, recoveryPasswordDescription: { - en: "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.", - args: undefined, + en: "Use your recovery password to load your account on new devices.

Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.", + args: undefined, }, recoveryPasswordEnter: { - en: "Enter your recovery password", - args: undefined, + en: 'Enter your recovery password', + args: undefined, }, recoveryPasswordErrorLoad: { - en: "An error occurred when trying to load your recovery password.

Please export your logs, then upload the file through the Session Help Desk to help resolve this issue.", - args: undefined, + en: 'An error occurred when trying to load your recovery password.

Please export your logs, then upload the file through the Session Help Desk to help resolve this issue.', + args: undefined, }, recoveryPasswordErrorMessageGeneric: { - en: "Please check your recovery password and try again.", - args: undefined, + en: 'Please check your recovery password and try again.', + args: undefined, }, recoveryPasswordErrorMessageIncorrect: { - en: "Some of the words in your Recovery Password are incorrect. Please check and try again.", - args: undefined, + en: 'Some of the words in your Recovery Password are incorrect. Please check and try again.', + args: undefined, }, recoveryPasswordErrorMessageShort: { - en: "The Recovery Password you entered is not long enough. Please check and try again.", - args: undefined, + en: 'The Recovery Password you entered is not long enough. Please check and try again.', + args: undefined, }, recoveryPasswordErrorTitle: { - en: "Incorrect Recovery Password", - args: undefined, + en: 'Incorrect Recovery Password', + args: undefined, }, recoveryPasswordExplanation: { - en: "To load your account, enter your recovery password.", - args: undefined, + en: 'To load your account, enter your recovery password.', + args: undefined, }, recoveryPasswordHidePermanently: { - en: "Hide Recovery Password Permanently", - args: undefined, + en: 'Hide Recovery Password Permanently', + args: undefined, }, recoveryPasswordHidePermanentlyDescription1: { - en: "Without your recovery password, you cannot load your account on new devices.

We strongly recommend you save your recovery password in a safe and secure place before continuing.", - args: undefined, + en: 'Without your recovery password, you cannot load your account on new devices.

We strongly recommend you save your recovery password in a safe and secure place before continuing.', + args: undefined, }, recoveryPasswordHidePermanentlyDescription2: { - en: "Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.", - args: undefined, + en: 'Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.', + args: undefined, }, recoveryPasswordHideRecoveryPassword: { - en: "Hide Recovery Password", - args: undefined, + en: 'Hide Recovery Password', + args: undefined, }, recoveryPasswordHideRecoveryPasswordDescription: { - en: "Permanently hide your recovery password on this device.", - args: undefined, + en: 'Permanently hide your recovery password on this device.', + args: undefined, }, recoveryPasswordRestoreDescription: { - en: "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.", - args: undefined, + en: "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.", + args: undefined, }, recoveryPasswordView: { - en: "View Password", - args: undefined, + en: 'View Password', + args: undefined, }, recoveryPasswordWarningSendDescription: { - en: "This is your recovery password. If you send it to someone they'll have full access to your account.", - args: undefined, + en: "This is your recovery password. If you send it to someone they'll have full access to your account.", + args: undefined, }, recreateGroup: { - en: "Recreate Group", - args: undefined, + en: 'Recreate Group', + args: undefined, }, redo: { - en: "Redo", - args: undefined, + en: 'Redo', + args: undefined, }, remainingCharactersOverTooltip: { - en: "Reduce message length by {count}", - args: {count: "number"} + en: 'Reduce message length by {count}', + args: { count: 'number' }, }, remove: { - en: "Remove", - args: undefined, + en: 'Remove', + args: undefined, }, removePasswordFail: { - en: "Failed to remove password", - args: undefined, + en: 'Failed to remove password', + args: undefined, }, reply: { - en: "Reply", - args: undefined, + en: 'Reply', + args: undefined, }, resend: { - en: "Resend", - args: undefined, + en: 'Resend', + args: undefined, }, resolving: { - en: "Loading country information...", - args: undefined, + en: 'Loading country information...', + args: undefined, }, restart: { - en: "Restart", - args: undefined, + en: 'Restart', + args: undefined, }, resync: { - en: "Resync", - args: undefined, + en: 'Resync', + args: undefined, }, retry: { - en: "Retry", - args: undefined, + en: 'Retry', + args: undefined, }, save: { - en: "Save", - args: undefined, + en: 'Save', + args: undefined, }, saved: { - en: "Saved", - args: undefined, + en: 'Saved', + args: undefined, }, savedMessages: { - en: "Saved messages", - args: undefined, + en: 'Saved messages', + args: undefined, }, saving: { - en: "Saving...", - args: undefined, + en: 'Saving...', + args: undefined, }, scan: { - en: "Scan", - args: undefined, + en: 'Scan', + args: undefined, }, screenSecurity: { - en: "Screen Security", - args: undefined, + en: 'Screen Security', + args: undefined, }, screenshotNotifications: { - en: "Screenshot Notifications", - args: undefined, + en: 'Screenshot Notifications', + args: undefined, }, screenshotNotificationsDescription: { - en: "Require a notification when a contact takes a screenshot of a one-to-one chat.", - args: undefined, + en: 'Require a notification when a contact takes a screenshot of a one-to-one chat.', + args: undefined, }, screenshotTaken: { - en: "{name} took a screenshot.", - args: {name: "string"} + en: '{name} took a screenshot.', + args: { name: 'string' }, }, search: { - en: "Search", - args: undefined, + en: 'Search', + args: undefined, }, searchContacts: { - en: "Search Contacts", - args: undefined, + en: 'Search Contacts', + args: undefined, }, searchConversation: { - en: "Search Conversation", - args: undefined, + en: 'Search Conversation', + args: undefined, }, searchEnter: { - en: "Please enter your search.", - args: undefined, + en: 'Please enter your search.', + args: undefined, }, searchMatchesNone: { - en: "No results found.", - args: undefined, + en: 'No results found.', + args: undefined, }, searchMatchesNoneSpecific: { - en: "No results found for {query}", - args: {query: "string"} + en: 'No results found for {query}', + args: { query: 'string' }, }, searchMembers: { - en: "Search Members", - args: undefined, + en: 'Search Members', + args: undefined, }, searchSearching: { - en: "Searching...", - args: undefined, + en: 'Searching...', + args: undefined, }, select: { - en: "Select", - args: undefined, + en: 'Select', + args: undefined, }, selectAll: { - en: "Select All", - args: undefined, + en: 'Select All', + args: undefined, }, selectAppIcon: { - en: "Select app icon", - args: undefined, + en: 'Select app icon', + args: undefined, }, send: { - en: "Send", - args: undefined, + en: 'Send', + args: undefined, }, sending: { - en: "Sending", - args: undefined, + en: 'Sending', + args: undefined, }, sendingCallOffer: { - en: "Sending Call Offer", - args: undefined, + en: 'Sending Call Offer', + args: undefined, }, sendingConnectionCandidates: { - en: "Sending Connection Candidates", - args: undefined, + en: 'Sending Connection Candidates', + args: undefined, }, sent: { - en: "Sent:", - args: undefined, + en: 'Sent:', + args: undefined, }, sessionAppearance: { - en: "Appearance", - args: undefined, + en: 'Appearance', + args: undefined, }, sessionClearData: { - en: "Clear Data", - args: undefined, + en: 'Clear Data', + args: undefined, }, sessionConversations: { - en: "Conversations", - args: undefined, + en: 'Conversations', + args: undefined, }, sessionHelp: { - en: "Help", - args: undefined, + en: 'Help', + args: undefined, }, sessionInviteAFriend: { - en: "Invite a Friend", - args: undefined, + en: 'Invite a Friend', + args: undefined, }, sessionMessageRequests: { - en: "Message Requests", - args: undefined, + en: 'Message Requests', + args: undefined, }, sessionNetworkCurrentPrice: { - en: "Current SESH price", - args: undefined, + en: 'Current SESH price', + args: undefined, }, 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}", - args: {icon: "string"} + 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}', + args: { icon: 'string' }, }, sessionNetworkLearnAboutStaking: { - en: "Learn About Staking", - args: undefined, + en: 'Learn About Staking', + args: undefined, }, sessionNetworkMarketCap: { - en: "Market Cap", - args: undefined, + en: 'Market Cap', + args: undefined, }, sessionNetworkNodesSecuring: { - en: "Session Nodes securing your messages", - args: undefined, + en: 'Session Nodes securing your messages', + args: undefined, }, sessionNetworkNodesSwarm: { - en: "Session Nodes in your swarm", - args: undefined, + en: 'Session Nodes in your swarm', + args: undefined, }, sessionNetworkNotificationLive: { - en: "Session Token is live! Explore the new Session Network section in Settings to learn how Session Token powers Session.", - args: undefined, + en: 'Session Token is live! Explore the new Session Network section in Settings to learn how Session Token powers Session.', + args: undefined, }, sessionNetworkSecuredBy: { - en: "Network secured by", - args: undefined, + en: 'Network secured by', + args: undefined, }, sessionNetworkTokenDescription: { - en: "When you stake Session Token to secure the network, you earn rewards in SESH from the Staking Reward Pool.", - args: undefined, + en: 'When you stake Session Token to secure the network, you earn rewards in SESH from the Staking Reward Pool.', + args: undefined, }, sessionNew: { - en: "• New", - args: undefined, + en: '• New', + args: undefined, }, sessionNotifications: { - en: "Notifications", - args: undefined, + en: 'Notifications', + args: undefined, }, sessionPermissions: { - en: "Permissions", - args: undefined, + en: 'Permissions', + args: undefined, }, sessionPrivacy: { - en: "Privacy", - args: undefined, + en: 'Privacy', + args: undefined, }, sessionRecoveryPassword: { - en: "Recovery Password", - args: undefined, + en: 'Recovery Password', + args: undefined, }, sessionSettings: { - en: "Settings", - args: undefined, + en: 'Settings', + args: undefined, }, set: { - en: "Set", - args: undefined, + en: 'Set', + args: undefined, }, setCommunityDisplayPicture: { - en: "Set Community Display Picture", - args: undefined, + en: 'Set Community Display Picture', + args: undefined, }, settingsRestartDescription: { - en: "You must restart Session to apply your new settings.", - args: undefined, + en: 'You must restart Session to apply your new settings.', + args: undefined, }, share: { - en: "Share", - args: undefined, + en: 'Share', + args: undefined, }, shareAccountIdDescription: { - en: "Invite your friend to chat with you on Session by sharing your Account ID with them.", - args: undefined, + en: 'Invite your friend to chat with you on Session by sharing your Account ID with them.', + args: undefined, }, shareAccountIdDescriptionCopied: { - en: "Share with your friends wherever you usually speak with them — then move the conversation here.", - args: undefined, + en: 'Share with your friends wherever you usually speak with them — then move the conversation here.', + args: undefined, }, shareExtensionDatabaseError: { - en: "There is an issue opening the database. Please restart the app and try again.", - args: undefined, + en: 'There is an issue opening the database. Please restart the app and try again.', + args: undefined, }, shareExtensionNoAccountError: { - en: "Oops! Looks like you don't have a Session account yet.

You'll need to create one in the Session app before you can share.", - args: undefined, + en: "Oops! Looks like you don't have a Session account yet.

You'll need to create one in the Session app before you can share.", + args: undefined, }, shareToSession: { - en: "Share to Session", - args: undefined, + en: 'Share to Session', + args: undefined, }, show: { - en: "Show", - args: undefined, + en: 'Show', + args: undefined, }, showAll: { - en: "Show All", - args: undefined, + en: 'Show All', + args: undefined, }, showLess: { - en: "Show Less", - args: undefined, + en: 'Show Less', + args: undefined, }, showNoteToSelf: { - en: "Show Note to Self", - args: undefined, + en: 'Show Note to Self', + args: undefined, }, showNoteToSelfDescription: { - en: "Are you sure you want to show Note to Self in your conversation list?", - args: undefined, + en: 'Are you sure you want to show Note to Self in your conversation list?', + args: undefined, }, stickers: { - en: "Stickers", - args: undefined, + en: 'Stickers', + args: undefined, }, supportGoTo: { - en: "Go to Support Page", - args: undefined, + en: 'Go to Support Page', + args: undefined, }, systemInformationDesktop: { - en: "System Information: {information}", - args: {information: "string"} + en: 'System Information: {information}', + args: { information: 'string' }, }, tapToRetry: { - en: "Tap to retry", - args: undefined, + en: 'Tap to retry', + args: undefined, }, theContinue: { - en: "Continue", - args: undefined, + en: 'Continue', + args: undefined, }, theDefault: { - en: "Default", - args: undefined, + en: 'Default', + args: undefined, }, theError: { - en: "Error", - args: undefined, + en: 'Error', + args: undefined, }, tryAgain: { - en: "Try Again", - args: undefined, + en: 'Try Again', + args: undefined, }, typingIndicators: { - en: "Typing Indicators", - args: undefined, + en: 'Typing Indicators', + args: undefined, }, typingIndicatorsDescription: { - en: "See and share typing indicators.", - args: undefined, + en: 'See and share typing indicators.', + args: undefined, }, unavailable: { - en: "Unavailable", - args: undefined, + en: 'Unavailable', + args: undefined, }, undo: { - en: "Undo", - args: undefined, + en: 'Undo', + args: undefined, }, unknown: { - en: "Unknown", - args: undefined, + en: 'Unknown', + args: undefined, }, updateApp: { - en: "App updates", - args: undefined, + en: 'App updates', + args: undefined, }, updateDownloaded: { - en: "Update installed, click to restart", - args: undefined, + en: 'Update installed, click to restart', + args: undefined, }, updateDownloading: { - en: "Downloading update: {percent_loader}%", - args: {percent_loader: "string"} + en: 'Downloading update: {percent_loader}%', + args: { percent_loader: 'string' }, }, updateError: { - en: "Cannot Update", - args: undefined, + en: 'Cannot Update', + args: undefined, }, updateErrorDescription: { - en: "Session failed to update. Please go to https://getsession.org/download and install the new version manually, then contact our Help Center to let us know about this problem.", - args: undefined, + en: 'Session failed to update. Please go to https://getsession.org/download and install the new version manually, then contact our Help Center to let us know about this problem.', + args: undefined, }, updateGroupInformation: { - en: "Update Group Information", - args: undefined, + en: 'Update Group Information', + args: undefined, }, updateGroupInformationDescription: { - en: "Group name and description are visible to all group members.", - args: undefined, + en: 'Group name and description are visible to all group members.', + args: undefined, }, updateGroupInformationEnterShorterDescription: { - en: "Please enter a shorter group description", - args: undefined, + en: 'Please enter a shorter group description', + args: undefined, }, updateNewVersion: { - en: "A new version of Session is available, tap to update", - args: undefined, + en: 'A new version of Session is available, tap to update', + args: undefined, }, updateNewVersionDescription: { - en: "A new version ({version}) of Session is available.", - args: {version: "string"} + en: 'A new version ({version}) of Session is available.', + args: { version: 'string' }, }, updateReleaseNotes: { - en: "Go to Release Notes", - args: undefined, + en: 'Go to Release Notes', + args: undefined, }, updateSession: { - en: "Session Update", - args: undefined, + en: 'Session Update', + args: undefined, }, updateVersion: { - en: "Version {version}", - args: {version: "string"} + en: 'Version {version}', + args: { version: 'string' }, }, updated: { - en: "Last updated {relative_time} ago", - args: {relative_time: "string"} + en: 'Last updated {relative_time} ago', + args: { relative_time: 'string' }, }, uploading: { - en: "Uploading", - args: undefined, + en: 'Uploading', + args: undefined, }, urlCopy: { - en: "Copy URL", - args: undefined, + en: 'Copy URL', + args: undefined, }, urlOpen: { - en: "Open URL", - args: undefined, + en: 'Open URL', + args: undefined, }, urlOpenBrowser: { - en: "This will open in your browser.", - args: undefined, + en: 'This will open in your browser.', + args: undefined, }, urlOpenDescription: { - en: "Are you sure you want to open this URL in your browser?

{url}", - args: {url: "string"} + en: 'Are you sure you want to open this URL in your browser?

{url}', + args: { url: 'string' }, }, useFastMode: { - en: "Use Fast Mode", - args: undefined, + en: 'Use Fast Mode', + args: undefined, }, video: { - en: "Video", - args: undefined, + en: 'Video', + args: undefined, }, videoErrorPlay: { - en: "Unable to play video.", - args: undefined, + en: 'Unable to play video.', + args: undefined, }, view: { - en: "View", - args: undefined, + en: 'View', + args: undefined, }, viewLess: { - en: "View Less", - args: undefined, + en: 'View Less', + args: undefined, }, viewMore: { - en: "View More", - args: undefined, + en: 'View More', + args: undefined, }, waitFewMinutes: { - en: "This can take a few minutes.", - args: undefined, + en: 'This can take a few minutes.', + args: undefined, }, waitOneMoment: { - en: "One moment please...", - args: undefined, + en: 'One moment please...', + args: undefined, }, warning: { - en: "Warning", - args: undefined, + en: 'Warning', + args: undefined, }, window: { - en: "Window", - args: undefined, + en: 'Window', + args: undefined, }, yes: { - en: "Yes", - args: undefined, + en: 'Yes', + args: undefined, }, you: { - en: "You", - args: undefined, + en: 'You', + args: undefined, }, sessionNetworkDataPrice: { - en: "Price data powered by CoinGecko
Accurate at {date_time}", - args: {date_time: "string"} + en: 'Price data powered by CoinGecko
Accurate at {date_time}', + args: { date_time: 'string' }, }, } as const; export const pluralsDictionary = { adminSendingPromotion: { - en:{ - one: "Sending admin promotion", - other: "Sending admin promotions" + en: { + one: 'Sending admin promotion', + other: 'Sending admin promotions', }, - args: {count: "number"} + args: { count: 'number' }, }, clearDataErrorDescription: { - en:{ - one: "Data not deleted by {count} Service Node. Service Node ID: {service_node_id}.", - other: "Data not deleted by {count} Service Nodes. Service Node IDs: {service_node_id}." + en: { + one: 'Data not deleted by {count} Service Node. Service Node ID: {service_node_id}.', + other: 'Data not deleted by {count} Service Nodes. Service Node IDs: {service_node_id}.', }, - args: {count: "number", service_node_id: "string"} + args: { count: 'number', service_node_id: 'string' }, }, deleteMessage: { - en:{ - one: "Delete Message", - other: "Delete Messages" + en: { + one: 'Delete Message', + other: 'Delete Messages', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageConfirm: { - en:{ - one: "Are you sure you want to delete this message?", - other: "Are you sure you want to delete these messages?" + en: { + one: 'Are you sure you want to delete this message?', + other: 'Are you sure you want to delete these messages?', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageDeleted: { - en:{ - one: "Message deleted", - other: "Messages deleted" + en: { + one: 'Message deleted', + other: 'Messages deleted', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageDescriptionDevice: { - en:{ - one: "Are you sure you want to delete this message from this device only?", - other: "Are you sure you want to delete these messages from this device only?" + en: { + one: 'Are you sure you want to delete this message from this device only?', + other: 'Are you sure you want to delete these messages from this device only?', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageFailed: { - en:{ - one: "Failed to delete message", - other: "Failed to delete messages" + en: { + one: 'Failed to delete message', + other: 'Failed to delete messages', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageNoteToSelfWarning: { - en:{ - one: "This message cannot be deleted from all your devices", - other: "Some of the messages you have selected cannot be deleted from all your devices" + en: { + one: 'This message cannot be deleted from all your devices', + other: 'Some of the messages you have selected cannot be deleted from all your devices', }, - args: {count: "number"} + args: { count: 'number' }, }, deleteMessageWarning: { - en:{ - one: "This message cannot be deleted for everyone", - other: "Some of the messages you have selected cannot be deleted for everyone" + en: { + one: 'This message cannot be deleted for everyone', + other: 'Some of the messages you have selected cannot be deleted for everyone', }, - args: {count: "number"} + args: { count: 'number' }, }, emojiReactsCountOthers: { - en:{ - one: "And {count} other has reacted {emoji} to this message.", - other: "And {count} others have reacted {emoji} to this message." + en: { + one: 'And {count} other has reacted {emoji} to this message.', + other: 'And {count} others have reacted {emoji} to this message.', }, - args: {count: "number", emoji: "string"} + args: { count: 'number', emoji: 'string' }, }, groupInviteSending: { - en:{ - one: "Sending invite", - other: "Sending invites" + en: { + one: 'Sending invite', + other: 'Sending invites', }, - args: {count: "number"} + args: { count: 'number' }, }, groupRemoveMessages: { - en:{ - one: "Remove user and their messages", - other: "Remove users and their messages" + en: { + one: 'Remove user and their messages', + other: 'Remove users and their messages', }, - args: {count: "number"} + args: { count: 'number' }, }, groupRemoveUserOnly: { - en:{ - one: "Remove user", - other: "Remove users" + en: { + one: 'Remove user', + other: 'Remove users', }, - args: {count: "number"} + args: { count: 'number' }, }, inviteFailed: { - en:{ - one: "Invite Failed", - other: "Invites Failed" + en: { + one: 'Invite Failed', + other: 'Invites Failed', }, - args: {count: "number"} + args: { count: 'number' }, }, inviteFailedDescription: { - en:{ - one: "The invite could not be sent. Would you like to try again?", - other: "The invites could not be sent. Would you like to try again?" + en: { + one: 'The invite could not be sent. Would you like to try again?', + other: 'The invites could not be sent. Would you like to try again?', }, - args: {count: "number"} + args: { count: 'number' }, }, members: { - en:{ - one: "{count} member", - other: "{count} members" + en: { + one: '{count} member', + other: '{count} members', }, - args: {count: "number"} + args: { count: 'number' }, }, membersActive: { - en:{ - one: "{count} active member", - other: "{count} active members" + en: { + one: '{count} active member', + other: '{count} active members', }, - args: {count: "number"} + args: { count: 'number' }, }, membersInviteSend: { - en:{ - one: "Send Invite", - other: "Send Invites" + en: { + one: 'Send Invite', + other: 'Send Invites', }, - args: {count: "number"} + args: { count: 'number' }, }, messageNew: { - en:{ - one: "New Message", - other: "New Messages" + en: { + one: 'New Message', + other: 'New Messages', }, - args: {count: "number"} + args: { count: 'number' }, }, messageNewYouveGot: { - en:{ + en: { one: "You've got a new message.", - other: "You've got {count} new messages." + other: "You've got {count} new messages.", }, - args: {count: "number"} + args: { count: 'number' }, }, messageNewYouveGotGroup: { - en:{ + en: { one: "You've got a new message in {group_name}.", - other: "You've got {count} new messages in {group_name}." + other: "You've got {count} new messages in {group_name}.", }, - args: {group_name: "string", count: "number"} + args: { group_name: 'string', count: 'number' }, }, modalMessageCharacterDisplayDescription: { - en:{ - one: "Messages have a character limit of {limit} characters. You have {count} character remaining", - other: "Messages have a character limit of {limit} characters. You have {count} characters remaining" + en: { + one: 'Messages have a character limit of {limit} characters. You have {count} character remaining', + other: + 'Messages have a character limit of {limit} characters. You have {count} characters remaining', }, - args: {limit: "string", count: "number"} + args: { limit: 'string', count: 'number' }, }, promotionFailed: { - en:{ - one: "Promotion Failed", - other: "Promotions Failed" + en: { + one: 'Promotion Failed', + other: 'Promotions Failed', }, - args: {count: "number"} + args: { count: 'number' }, }, promotionFailedDescription: { - en:{ - one: "The promotion could not be applied. Would you like to try again?", - other: "The promotions could not be applied. Would you like to try again?" + en: { + one: 'The promotion could not be applied. Would you like to try again?', + other: 'The promotions could not be applied. Would you like to try again?', }, - args: {count: "number"} + args: { count: 'number' }, }, remainingCharactersTooltip: { - en:{ - one: "{count} character remaining", - other: "{count} characters remaining" + en: { + one: '{count} character remaining', + other: '{count} characters remaining', }, - args: {count: "number"} + args: { count: 'number' }, }, searchMatches: { - en:{ - one: "{found_count} of {count} match", - other: "{found_count} of {count} matches" + en: { + one: '{found_count} of {count} match', + other: '{found_count} of {count} matches', }, - args: {found_count: "number", count: "number"} + args: { found_count: 'number', count: 'number' }, }, } as const; diff --git a/run/test/specs/check_avatar_color.spec.ts b/run/test/specs/check_avatar_color.spec.ts index f40bf3df..8a419097 100644 --- a/run/test/specs/check_avatar_color.spec.ts +++ b/run/test/specs/check_avatar_color.spec.ts @@ -1,16 +1,23 @@ -import { bothPlatformsIt } from '../../types/sessionIt'; +import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; import { isSameColor } from './utils/check_colour'; import { UserSettings } from './locators/settings'; -import { ConversationAvatar, ConversationSettings } from './locators/conversation'; +import { ConversationSettings } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { ConversationItem } from './locators/home'; -bothPlatformsIt({ +bothPlatformsItSeparate({ title: 'Avatar color', risk: 'medium', - testCb: avatarColor, countOfDevicesNeeded: 2, + ios: { + testCb: avatarColor, + shouldSkip: false, + }, + android: { + testCb: avatarColor, + shouldSkip: true, // something is going on on Android, test is picking up wildly different pixel colors + }, }); async function avatarColor(platform: SupportedPlatformsType) { @@ -22,20 +29,15 @@ async function avatarColor(platform: SupportedPlatformsType) { focusFriendsConvo: false, }); - // Get Alice's avatar color on device 1 (Home Screen avatar) and turn it into a hex value + // Get Alice's avatar color on device 1 (Settings screen avatar) and turn it into a hex value + await alice1.clickOnElementAll(new UserSettings(alice1)); const alice1PixelColor = await alice1.getElementPixelColor(new UserSettings(alice1)); + console.log(alice1PixelColor); // Get Alice's avatar color on device 2 and turn it into a hex value - let bob1PixelColor; // Open the conversation with Alice on Bob's device await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); - // The conversation screen looks slightly different per platform so we're grabbing the avatar from different locators - // On iOS the avatar doubles as the Conversation Settings button on the right - // On Android, the avatar is a separate, non-interactable element on the left (and the settings has the 3-dot icon) - if (platform === 'ios') { - bob1PixelColor = await bob1.getElementPixelColor(new ConversationSettings(bob1)); - } else { - bob1PixelColor = await bob1.getElementPixelColor(new ConversationAvatar(bob1)); - } + const bob1PixelColor = await bob1.getElementPixelColor(new ConversationSettings(bob1)); + console.log(bob1PixelColor); // Color matching devices 1 and 2 const colorMatch = isSameColor(alice1PixelColor, bob1PixelColor); if (!colorMatch) { diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index 35472b19..feb50fe7 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -2,6 +2,7 @@ import { testCommunityLink, testCommunityName } from '../../constants/community' import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationItem } from './locators/home'; import { open_Alice2 } from './state_builder'; +import { sleepFor } from './utils'; import { joinCommunity } from './utils/join_community'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -19,7 +20,8 @@ async function joinCommunityTest(platform: SupportedPlatformsType) { const testMessage = `Test message + ${new Date().getTime()}`; await joinCommunity(alice1, testCommunityLink, testCommunityName); - await alice1.onIOS().scrollToBottom(); + await sleepFor(5000); + await alice1.scrollToBottom(); await alice1.sendMessage(testMessage); // Has community synced to device 2? await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 7a43b5d6..94ffd8b8 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -8,6 +8,7 @@ import { setDisappearingMessage } from './utils/set_disappearing_messages'; import { testCommunityLink, testCommunityName } from './../../constants/community'; import { ConversationSettings } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; +import { GroupMember } from './locators/groups'; bothPlatformsItSeparate({ title: 'Disappearing community invite message 1:1', @@ -41,11 +42,7 @@ async function disappearingCommunityInviteMessageIos(platform: SupportedPlatform await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(1000); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); - await alice1.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); + await alice1.clickOnElementAll(new GroupMember(alice1).build(bob.userName)); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Invite contacts button', @@ -91,12 +88,10 @@ async function disappearingCommunityInviteMessageAndroid(platform: SupportedPlat await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(1000); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); - await alice1.clickOnElementByText({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); - await alice1.clickOnByAccessibilityID('Done'); + await alice1.clickOnElementAll(new GroupMember(alice1).build(bob.userName)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'invite-contacts-button'}); // Check device 2 for invitation from user A await bob1.waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index 2262d959..2d15d2b7 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -90,7 +90,6 @@ async function disappearingLinkMessage1o1Android(platform: SupportedPlatformsTyp focusFriendsConvo: true, }); await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); - // await alice1.navigateBack(); // Send a link await alice1.inputText(testLink, { strategy: 'accessibility id', @@ -100,18 +99,16 @@ async function disappearingLinkMessage1o1Android(platform: SupportedPlatformsTyp await alice1.checkModalStrings( englishStrippedStr('linkPreviewsEnable').toString(), englishStrippedStr('linkPreviewsFirstDescription').toString(), - true + false ); await alice1.clickOnByAccessibilityID('Enable'); - // No preview on first send - // Wait for preview to load - await sleepFor(2000); + // Preview takes a while to load + await sleepFor(5000); await alice1.clickOnByAccessibilityID('Send message button'); await alice1.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(alice1).build(), maxWait: 20000, }); - // Send again for image // Make sure image preview is available in device 2 await bob1.waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index a6e36a13..c5c4ebb2 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -114,11 +114,9 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType) { // 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({ - strategy: 'accessibility id', - selector: 'Conversation header name', - text: newGroupName, - }); + await device2.waitForTextElementToBePresent( + new ConversationHeaderName(device2).build(newGroupName) + ); await Promise.all([ device2.waitForControlMessageToBePresent(groupNameNew), device3.waitForControlMessageToBePresent(groupNameNew), diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 1e43d045..d0ed0344 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -1,5 +1,6 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { ConversationHeaderName } from './locators/conversation'; import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; @@ -29,11 +30,9 @@ async function restoreGroup(platform: SupportedPlatformsType) { // Check that group has loaded on linked device await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded - await device4.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Conversation header name', - text: testGroupName, - }); + await device4.waitForTextElementToBePresent( + new ConversationHeaderName(device4).build(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 97549dd9..41ef3436 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -26,21 +26,6 @@ export class ConversationSettings extends LocatorsInterface { } } -// android-only locator for the avatar -export class ConversationAvatar extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'network.loki.messenger:id/singleModeImageView', - } as const; - case 'ios': - throw new Error('Unsupported platform'); - } - } -} - export class DeletedMessage extends LocatorsInterface { public build() { return { @@ -164,9 +149,17 @@ export class NotificationSwitch extends LocatorsInterface { export class BlockedBanner extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Blocked banner', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'blocked-banner', // See SES-3992 + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Blocked banner', + } as const; + } } } diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 3cf186ff..2a865cb1 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -242,3 +242,19 @@ export class MemberStatus extends LocatorsInterface { } } } +export class ManageMembersMenuItem extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'manage-members-menu-option', + }; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Manage Members', + }; + } + } +} \ No newline at end of file diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index c23579b9..27d99fec 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -303,8 +303,7 @@ export class InviteContactsMenuItem extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/title', - text: 'Invite Contacts', + selector: 'invite-contacts-menu-option', }; case 'ios': return { diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 3cd78292..cb61bc75 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -49,7 +49,7 @@ export class RecoveryPasswordMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', + strategy: 'id', selector: 'Recovery password menu item', } as const; case 'ios': @@ -161,7 +161,7 @@ export class PrivacyMenuItem extends LocatorsInterface { } export class ConversationsMenuItem extends LocatorsInterface { - public build() { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index b0c18e1d..68fc39d4 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -8,6 +8,7 @@ import { ConversationSettings } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { ConversationItem } from './locators/home'; +import { GroupMember } from './locators/groups'; bothPlatformsItSeparate({ title: 'Send community invitation', @@ -87,12 +88,10 @@ async function sendCommunityInviteMessageAndroid(platform: SupportedPlatformsTyp // Add user B to community await alice1.clickOnElementAll(new ConversationSettings(alice1)); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); - await alice1.clickOnElementByText({ - strategy: 'accessibility id', - selector: 'Contact', - text: bob.userName, - }); - await alice1.clickOnByAccessibilityID('Done'); + await alice1.clickOnElementAll(new GroupMember(alice1).build(bob.userName)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'invite-contacts-button'}); // Check device 2 for invitation from user A await bob1.waitForTextElementToBePresent({ strategy: 'id', @@ -110,7 +109,7 @@ async function sendCommunityInviteMessageAndroid(platform: SupportedPlatformsTyp englishStrippedStr('communityJoinDescription') .withArgs({ community_name: testCommunityName }) .toString(), - true + false ); await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Join' }); await bob1.navigateBack(); diff --git a/run/test/specs/message_deletion.spec.ts b/run/test/specs/message_deletion.spec.ts index f2f3667d..c629c797 100644 --- a/run/test/specs/message_deletion.spec.ts +++ b/run/test/specs/message_deletion.spec.ts @@ -34,7 +34,7 @@ async function deleteMessage(platform: SupportedPlatformsType) { englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() ); // Select 'Delete on this device only' - await alice1.clickOnElementAll(new DeleteMessageLocally(alice1)); + await alice1.clickOnElementAll(new DeleteMessageLocally(alice1)); // This is currently missing an AX ID on Android await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); // Device 1 should show 'Deleted message' message diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index 6bd018b0..5b792359 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -41,6 +41,7 @@ async function acceptRequest(platform: SupportedPlatformsType) { ]); // Check conversation list for new contact (user A) await device2.navigateBack(); + await device2.navigateBack(false); await Promise.all([ device2.waitForTextElementToBePresent({ strategy: 'accessibility id', diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index a690ae91..22d29ec3 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -29,8 +29,8 @@ async function deleteRequest(platform: SupportedPlatformsType) { await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); await device2.checkModalStrings( englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString(), - true + englishStrippedStr('messageRequestsContactDelete').toString(), + false ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", 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 cd98903f..df362006 100644 --- a/run/test/specs/user_actions_block_conversation_list.spec.ts +++ b/run/test/specs/user_actions_block_conversation_list.spec.ts @@ -6,7 +6,6 @@ import { LongPressBlockOption } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { open_Alice1_Bob1_friends } from './state_builder'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; -import { BlockedBanner } from './locators/conversation'; // Block option no longer available on iOS in conversation list androidIt({ @@ -31,16 +30,17 @@ async function blockUserInConversationList(platform: SupportedPlatformsType) { await alice1.checkModalStrings( englishStrippedStr('block').toString(), englishStrippedStr('blockDescription').withArgs({ name: USERNAME.BOB }).toString(), - true + false ); - await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); - await alice1.clickOnElementAll({ + await alice1.onIOS().clickOnElementAll(new BlockUserConfirmationModal(alice1)); + await alice1.onAndroid().clickOnByAccessibilityID('Block'); // This is an old modal so the locator class cannot be used + // Once you block the conversation disappears from the home screen + await alice1.hasElementBeenDeleted({ strategy: 'accessibility id', selector: 'Conversation list item', text: bob.userName, + maxWait: 5000, }); - await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); - await alice1.navigateBack(); await alice1.clickOnElementAll(new UserSettings(alice1)); // 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it await alice1.scrollDown(); 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 4e70079a..da40b025 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -45,7 +45,10 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) await alice1.navigateBack(); // Look for alert at top of screen (Bob is blocked. Unblock them?) // Check device 1 for blocked status - const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); + const blockedStatus = await alice1.waitForTextElementToBePresent({ + ...new BlockedBanner(alice1).build(), + maxWait: 5000, + }); if (blockedStatus) { console.info(`${bob.userName} has been blocked`); } else { 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 84b7e216..fd450dbf 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -1,4 +1,4 @@ -import { bothPlatformsIt } from '../../types/sessionIt'; +import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ImageName, MediaMessageInput, SendMediaButton, ShareExtensionIcon } from './locators'; import { PhotoLibrary } from './locators/external'; @@ -8,10 +8,17 @@ import { SupportedPlatformsType } from './utils/open_app'; import { testImage } from '../../constants/testfiles'; import { handlePhotosFirstTimeOpen } from './utils/handle_first_open'; -bothPlatformsIt({ +// TODO investigate why the Android Photos app throws an unexpected error when sharing +bothPlatformsItSeparate({ title: 'Share to session', risk: 'low', + ios: { testCb: shareToSession, + }, + android: { + testCb: shareToSession, + shouldSkip: true, + }, countOfDevicesNeeded: 2, }); diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index 50ebff2d..03b44f80 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -30,7 +30,10 @@ async function unblockUser(platform: SupportedPlatformsType) { ); await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); await alice1.onIOS().navigateBack(); - const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); + const blockedStatus = await alice1.waitForTextElementToBePresent({ + ...new BlockedBanner(alice1).build(), + maxWait: 5000, + }); if (blockedStatus) { console.info(`${bob.userName} has been blocked`); } else { diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 8af0f090..b50cfa47 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -39,9 +39,9 @@ export const newUser = async (device: DeviceWrapper, userName: UserNameType): Pr const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); // Exit Modal - await device.navigateBack(); + await device.navigateBack(false); await device.clickOnElementAll(new UserSettings(device)); const accountID = await device.grabTextFromAccessibilityId('Account ID'); - await device.closeScreen(); + await device.closeScreen(false); return { userName, accountID, recoveryPhrase }; }; diff --git a/run/test/specs/utils/join_community.ts b/run/test/specs/utils/join_community.ts index 55b861ae..1b845d6b 100644 --- a/run/test/specs/utils/join_community.ts +++ b/run/test/specs/utils/join_community.ts @@ -1,5 +1,6 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { CommunityInput, JoinCommunityButton } from '../locators'; +import { ConversationHeaderName } from '../locators/conversation'; import { PlusButton } from '../locators/home'; import { JoinCommunityOption } from '../locators/start_conversation'; @@ -12,9 +13,7 @@ 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({ - strategy: 'accessibility id', - selector: 'Conversation header name', - text: communityName, - }); + await device.waitForTextElementToBePresent( + new ConversationHeaderName(device).build(communityName) + ); }; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 0fd4a53b..f6e5efa6 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -277,7 +277,7 @@ export class DeviceWrapper { * @param maxWait - Maximum wait time in milliseconds for each locator (default: 3000). * @throws If neither locator is found. */ - private async findWithFallback( + public async findWithFallback( primaryLocator: LocatorsInterface | StrategyExtractionObj, fallbackLocator: LocatorsInterface | StrategyExtractionObj, maxWait: number = 3000 @@ -1680,7 +1680,7 @@ export class DeviceWrapper { englishStrippedStr(`attachmentsAutoDownloadModalDescription`) .withArgs({ conversation_name: conversationName }) .toString(), - true + false ); await this.clickOnElementAll(new DownloadMediaButton(this)); } diff --git a/run/types/testing.ts b/run/types/testing.ts index 9394d4aa..59e17d4f 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -363,7 +363,8 @@ export type AccessibilityId = | 'Appearance' | 'Select alternate app icon' | 'MeetingSE' - | 'Donate'; + | 'Donate' + | 'Manage Members'; export type Id = | 'Modal heading' @@ -469,7 +470,12 @@ export type Id = | 'Notifications' | 'All Session notifications' | 'com.android.settings:id/switch_text' - | 'Block'; + | 'Block' + | 'blocked-banner' + | 'invite-contacts-menu-option' + | 'invite-contacts-button' + | 'Recovery password menu item' + | 'manage-members-menu-option'; export type TestRisk = 'high' | 'medium' | 'low'; From bb7e685f24f7bbd774662c463c3eaf07bcc84d74 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Jun 2025 14:37:20 +1000 Subject: [PATCH 19/60] More Android 1.25.0 changes --- .../specs/group_tests_change_group_name.spec.ts | 7 ++----- run/test/specs/locators/conversation.ts | 4 ++-- run/test/specs/locators/index.ts | 13 ++++++------- run/test/specs/message_deletion.spec.ts | 2 +- run/test/specs/message_requests_block.spec.ts | 9 ++++----- run/test/specs/user_actions_unblock_user.spec.ts | 4 ++-- run/test/specs/utils/restore_account.ts | 2 +- run/types/DeviceWrapper.ts | 8 ++++---- run/types/testing.ts | 9 +++++++-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/run/test/specs/group_tests_change_group_name.spec.ts b/run/test/specs/group_tests_change_group_name.spec.ts index fb28a07c..c2faf092 100644 --- a/run/test/specs/group_tests_change_group_name.spec.ts +++ b/run/test/specs/group_tests_change_group_name.spec.ts @@ -1,5 +1,5 @@ import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { EditGroup, EditGroupName } from './locators'; +import { EditGroupName, EditGroup } from './locators'; import { EditGroupNameInput } from './locators/groups'; import { sleepFor } from './utils'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; @@ -83,13 +83,10 @@ async function changeGroupNameAndroid(platform: SupportedPlatformsType) { // Click on Edit group option await sleepFor(1000); await alice1.clickOnElementAll(new EditGroup(alice1)); - // Click on current group name - await alice1.clickOnElementAll(new EditGroupName(alice1)); - // Enter new group name (same test tag for both name and input) await alice1.clickOnElementAll(new EditGroupName(alice1)); await alice1.inputText(newGroupName, new EditGroupName(alice1)); // Click done/apply - await alice1.clickOnByAccessibilityID('Confirm'); + await alice1.clickOnElementById('update-group-info-confirm-button'); await alice1.navigateBack(); // Check control message for changed name await alice1.waitForControlMessageToBePresent( diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 41ef3436..9c49fa4d 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -152,8 +152,8 @@ export class BlockedBanner extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'blocked-banner', // See SES-3992 + strategy: 'accessibility id', + selector: 'blocked-banner', } as const; case 'ios': return { diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 27d99fec..6872be02 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -67,8 +67,7 @@ export class EditGroup extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/title', - text: 'Edit group', + selector: 'edit-profile-icon' } as const; case 'ios': return { @@ -90,7 +89,7 @@ export class EditGroupName extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Group name', + selector: 'update-group-info-name-input', } as const; } } @@ -319,8 +318,8 @@ export class DeleteMessageLocally extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Delete on this device only', + strategy: 'id', + selector: 'delete-only-on-this-device', }; case 'ios': return { @@ -336,8 +335,8 @@ export class DeleteMessageForEveryone extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Delete for everyone', + strategy: 'id', + selector: 'delete-for-everyone', }; case 'ios': return { diff --git a/run/test/specs/message_deletion.spec.ts b/run/test/specs/message_deletion.spec.ts index c629c797..f2f3667d 100644 --- a/run/test/specs/message_deletion.spec.ts +++ b/run/test/specs/message_deletion.spec.ts @@ -34,7 +34,7 @@ async function deleteMessage(platform: SupportedPlatformsType) { englishStrippedStr('deleteMessageConfirm').withArgs({ count: 1 }).toString() ); // Select 'Delete on this device only' - await alice1.clickOnElementAll(new DeleteMessageLocally(alice1)); // This is currently missing an AX ID on Android + await alice1.clickOnElementAll(new DeleteMessageLocally(alice1)); await alice1.clickOnElementAll(new DeleteMessageConfirmationModal(alice1)); // Device 1 should show 'Deleted message' message diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index 322f11a4..edc0e6ae 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -1,7 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME, type AccessibilityId } from '../../types/testing'; -import { BlockedContactsSettings, BlockUserConfirmationModal } from './locators'; +import { BlockedContactsSettings } from './locators'; import { PlusButton } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; @@ -42,13 +42,12 @@ async function blockedRequest(platform: SupportedPlatformsType) { await device2.clickOnByAccessibilityID('Block message request'); // Confirm block on android await sleepFor(1000); - // TODO add check modal await device2.checkModalStrings( englishStrippedStr('block').toString(), englishStrippedStr('blockDescription').withArgs({ name: alice.userName }).toString(), - true + false ); - await device2.clickOnElementAll(new BlockUserConfirmationModal(device1)); + await device2.clickOnByAccessibilityID('Block'); // This is an old Android modal so can't use the modern locator class // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); await Promise.all([ @@ -63,7 +62,7 @@ async function blockedRequest(platform: SupportedPlatformsType) { ]); const blockedMessage = `"${alice.userName} to ${bob.userName} - shouldn't get through"`; await device1.sendMessage(blockedMessage); - await device2.navigateBack(); + await device2.navigateBack(false); await device2.waitForTextElementToBePresent(new PlusButton(device2)); // Need to wait to see if message gets through await sleepFor(5000); diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index 03b44f80..a03902f5 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -29,7 +29,7 @@ async function unblockUser(platform: SupportedPlatformsType) { true ); await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); - await alice1.onIOS().navigateBack(); + await alice1.navigateBack(); const blockedStatus = await alice1.waitForTextElementToBePresent({ ...new BlockedBanner(alice1).build(), maxWait: 5000, @@ -52,7 +52,7 @@ async function unblockUser(platform: SupportedPlatformsType) { await alice1.checkModalStrings( englishStrippedStr('blockUnblock').toString(), englishStrippedStr('blockUnblockName').withArgs({ name: bob.userName }).toString(), - true + false ); await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Unblock' }); await alice1.doesElementExist({ ...new BlockedBanner(alice1).build(), maxWait: 2000 }); diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index f43b2d33..e156318c 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -24,7 +24,7 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => { const displayName = await device.doesElementExist({ strategy: 'accessibility id', selector: 'Enter display name', - maxWait: 1000, + maxWait: 2000, }); if (displayName) { await device.inputText(user.userName, { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f6e5efa6..69a2d0d0 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -293,14 +293,14 @@ export class DeviceWrapper { } console.warn( - `[navigateBack] Could not find primary locator with '${primary.strategy}', falling back on '${fallback.strategy}'` + `[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; } - throw new Error(`[navigateBack] Could not find primary or fallback locator`); + throw new Error(`[findWithFallback] Could not find primary or fallback locator`); } public async longClick(element: AppiumNextElementType, durationMs: number) { @@ -1860,9 +1860,9 @@ export class DeviceWrapper { await this.clickOnElementAll(new PrivacyMenuItem(this)); await sleepFor(2000); await this.clickOnElementAll(new ReadReceiptsButton(this)); - await this.navigateBack(); + await this.navigateBack(false); await sleepFor(100); - await this.closeScreen(); + await this.closeScreen(false); } public async checkPermissions( diff --git a/run/types/testing.ts b/run/types/testing.ts index 59e17d4f..68c29632 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -364,6 +364,7 @@ export type AccessibilityId = | 'Select alternate app icon' | 'MeetingSE' | 'Donate' + | 'blocked-banner' | 'Manage Members'; export type Id = @@ -471,11 +472,15 @@ export type Id = | 'All Session notifications' | 'com.android.settings:id/switch_text' | 'Block' - | 'blocked-banner' | 'invite-contacts-menu-option' | 'invite-contacts-button' | 'Recovery password menu item' - | 'manage-members-menu-option'; + | 'manage-members-menu-option' + | 'delete-only-on-this-device' + | 'delete-for-everyone' + | 'edit-profile-icon' + | 'update-group-info-confirm-button' + | 'update-group-info-name-input'; export type TestRisk = 'high' | 'medium' | 'low'; From a59121aa70eb0ed5f4b82526a909a7aa016455ed Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Jun 2025 17:05:29 +1000 Subject: [PATCH 20/60] refactor: add test steps to create_account util --- run/test/specs/utils/create_account.ts | 77 ++++++++++++++------------ 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index b50cfa47..5aecea7b 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -7,41 +7,50 @@ import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locator import { UserSettings } from '../locators/settings'; import { ContinueButton } from '../locators/global'; import { CopyButton } from '../locators/start_conversation'; +import test from '@playwright/test'; export const newUser = async (device: DeviceWrapper, userName: UserNameType): Promise => { - // Click create session ID - await device.clickOnElementAll(new CreateAccountButton(device)); - // Input username - await device.inputText(userName, new DisplayNameInput(device)); - // Click continue - await device.clickOnElementAll(new ContinueButton(device)); - // Choose message notification options - // Want to choose 'Slow Mode' so notifications don't interrupt test - await device.clickOnElementAll(new SlowModeRadio(device)); - // Select Continue to save notification settings - await device.clickOnElementAll(new ContinueButton(device)); + return await test.step(`Create new user: ${userName}`, async () => { + let accountID = '' + let recoveryPhrase = ''; + await test.step('Tap Create Account button', async () => { + await device.clickOnElementAll(new CreateAccountButton(device)); + }); + await test.step('Enter Display Name', async () => { + await device.inputText(userName, new DisplayNameInput(device)); + await device.clickOnElementAll(new ContinueButton(device)); + }); + await test.step('Choose Slow Mode for Notifications', async () => { + // Want to choose 'Slow Mode' so notifications don't interrupt test + await device.clickOnElementAll(new SlowModeRadio(device)); + await device.clickOnElementAll(new ContinueButton(device)); + }); // TODO need to retry check every 1s for 5s - console.warn('about to look for Allow permission in 5s'); - await sleepFor(5000); - - await device.checkPermissions('Allow'); - console.warn('looked for Allow permission'); - await sleepFor(1000); - // Click on 'continue' button to open recovery phrase modal - await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); - await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); - //Save recovery password - const recoveryPhraseContainer = await device.clickOnElementAll( - new RecoveryPhraseContainer(device) - ); - await device.onAndroid().clickOnElementAll(new CopyButton(device)); - // Save recovery phrase as variable - const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); - console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); - // Exit Modal - await device.navigateBack(false); - await device.clickOnElementAll(new UserSettings(device)); - const accountID = await device.grabTextFromAccessibilityId('Account ID'); - await device.closeScreen(false); + await test.step('Handle permission prompt if visible', async () => { + console.warn('about to look for Allow permission in 5s'); + await sleepFor(5000); + await device.checkPermissions('Allow'); + console.warn('looked for Allow permission'); + await sleepFor(1000); + }); + await test.step(`Save user's recovery password`, async () => { + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); + await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + //Save recovery password + const recoveryPhraseContainer = await device.clickOnElementAll( + new RecoveryPhraseContainer(device) + ); + await device.onAndroid().clickOnElementAll(new CopyButton(device)); + // Save recovery phrase as variable + recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); + console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); + await device.navigateBack(false); + }); + await test.step(`Save user's Account ID`, async () => { + await device.clickOnElementAll(new UserSettings(device)); + accountID = await device.grabTextFromAccessibilityId('Account ID'); + await device.closeScreen(false); + }); return { userName, accountID, recoveryPhrase }; -}; +}); +}; \ No newline at end of file From cbb637bb0ca3980a71b9a52cddc61507e9dbf52f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Jun 2025 17:19:49 +1000 Subject: [PATCH 21/60] Even more Android 1.25.0 changes --- .../disappear_after_send_off_1o1.spec.ts | 4 +- .../disappearing_community_invite.spec.ts | 5 +- run/test/specs/disappearing_link.spec.ts | 2 +- .../specs/group_tests_add_contact.spec.ts | 6 +- .../group_tests_change_group_name.spec.ts | 22 ++--- .../group_tests_edit_group_banner.spec.ts | 5 +- .../group_tests_invite_contact_banner.spec.ts | 6 +- .../specs/group_tests_kick_member.spec.ts | 6 +- .../specs/linked_device_block_user.spec.ts | 5 +- .../specs/linked_device_create_group.spec.ts | 16 ++-- run/test/specs/locators/groups.ts | 66 +++++++++++---- run/test/specs/locators/index.ts | 34 -------- .../message_community_invitation.spec.ts | 5 +- .../specs/message_requests_decline.spec.ts | 6 +- .../user_actions_delete_conversation.spec.ts | 2 +- .../user_actions_share_to_session.spec.ts | 4 +- run/test/specs/utils/create_account.ts | 84 +++++++++---------- run/test/specs/utils/create_group.ts | 7 +- run/test/specs/utils/restore_account.ts | 29 +++---- run/types/testing.ts | 8 +- 20 files changed, 168 insertions(+), 154 deletions(-) diff --git a/run/test/specs/disappear_after_send_off_1o1.spec.ts b/run/test/specs/disappear_after_send_off_1o1.spec.ts index e4144c5e..f9eb356b 100644 --- a/run/test/specs/disappear_after_send_off_1o1.spec.ts +++ b/run/test/specs/disappear_after_send_off_1o1.spec.ts @@ -55,7 +55,7 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType) { await alice1.clickOnElementAll(new DisappearingMessagesMenuOption(alice1)); await alice1.clickOnElementAll(new DisableDisappearingMessages(alice1)); await alice1.clickOnElementAll(new SetDisappearMessagesButton(alice1)); - await alice1.onIOS().navigateBack(); + await alice1.navigateBack(); // Check control message for turning off disappearing messages // Check USER A'S CONTROL MESSAGE on device 1 and 3 (linked device) const disappearingMessagesTurnedOffYou = englishStrippedStr( @@ -76,7 +76,7 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType) { await bob1.checkModalStrings( englishStrippedStr('disappearingMessagesFollowSetting').toString(), englishStrippedStr('disappearingMessagesFollowSettingOff').toString(), - true + false ); await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Confirm' }); // Check conversation subtitle? diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 94ffd8b8..bb32c592 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -90,8 +90,9 @@ async function disappearingCommunityInviteMessageAndroid(platform: SupportedPlat await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); await alice1.clickOnElementAll(new GroupMember(alice1).build(bob.userName)); await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'invite-contacts-button'}); + strategy: 'id', + selector: 'invite-contacts-button', + }); // Check device 2 for invitation from user A await bob1.waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index 2d15d2b7..0cef35ef 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -102,7 +102,7 @@ async function disappearingLinkMessage1o1Android(platform: SupportedPlatformsTyp false ); await alice1.clickOnByAccessibilityID('Enable'); - // Preview takes a while to load + // Preview takes a while to load await sleepFor(5000); await alice1.clickOnByAccessibilityID('Send message button'); await alice1.waitForTextElementToBePresent({ diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 66eeecec..7f3ee5e3 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -1,10 +1,10 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { EditGroup, InviteContactsButton, InviteContactsMenuItem } from './locators'; +import { InviteContactsButton, InviteContactsMenuItem } from './locators'; import { ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; -import { InviteContactConfirm } from './locators/groups'; +import { InviteContactConfirm, ManageMembersMenuItem } from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; import { sleepFor } from './utils'; @@ -38,7 +38,7 @@ async function addContactToGroup(platform: SupportedPlatformsType) { // Click more options await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Select edit group - await alice1.clickOnElementAll(new EditGroup(alice1)); + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); await sleepFor(1000); // Add contact to group await alice1.onIOS().clickOnElementAll(new InviteContactsMenuItem(alice1)); diff --git a/run/test/specs/group_tests_change_group_name.spec.ts b/run/test/specs/group_tests_change_group_name.spec.ts index c2faf092..f79fbe58 100644 --- a/run/test/specs/group_tests_change_group_name.spec.ts +++ b/run/test/specs/group_tests_change_group_name.spec.ts @@ -1,10 +1,12 @@ import { bothPlatformsItSeparate } from '../../types/sessionIt'; -import { EditGroupName, EditGroup } from './locators'; -import { EditGroupNameInput } from './locators/groups'; +import { + UpdateGroupInformation, + EditGroupNameInput, + SaveGroupNameChangeButton, +} from './locators/groups'; import { sleepFor } from './utils'; import { SupportedPlatformsType, closeApp } from './utils/open_app'; import { ConversationSettings } from './locators/conversation'; -import { SaveNameChangeButton } from './locators/settings'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; @@ -36,7 +38,7 @@ async function changeGroupNameIos(platform: SupportedPlatformsType) { // Click on Edit group option await sleepFor(1000); // Click on current group name - await alice1.clickOnElementAll(new EditGroupName(alice1)); + await alice1.clickOnElementAll(new UpdateGroupInformation(alice1, testGroupName)); await alice1.checkModalStrings( englishStrippedStr(`updateGroupInformation`).toString(), englishStrippedStr(`updateGroupInformationDescription`).toString() @@ -55,11 +57,11 @@ async function changeGroupNameIos(platform: SupportedPlatformsType) { } await alice1.clickOnByAccessibilityID('Cancel'); // Enter new group name - await alice1.clickOnElementAll(new EditGroupName(alice1)); + await alice1.clickOnElementAll(new UpdateGroupInformation(alice1, testGroupName)); await alice1.deleteText(new EditGroupNameInput(alice1)); await alice1.inputText(newGroupName, new EditGroupNameInput(alice1)); // Click done/apply - await alice1.clickOnElementAll(new SaveNameChangeButton(alice1)); + await alice1.clickOnElementAll(new SaveGroupNameChangeButton(alice1)); await alice1.navigateBack(); await alice1.waitForControlMessageToBePresent( englishStrippedStr('groupNameNew').withArgs({ group_name: newGroupName }).toString() @@ -82,11 +84,11 @@ async function changeGroupNameAndroid(platform: SupportedPlatformsType) { await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Click on Edit group option await sleepFor(1000); - await alice1.clickOnElementAll(new EditGroup(alice1)); - await alice1.clickOnElementAll(new EditGroupName(alice1)); - await alice1.inputText(newGroupName, new EditGroupName(alice1)); + await alice1.clickOnElementAll(new UpdateGroupInformation(alice1)); + await alice1.clickOnElementAll(new EditGroupNameInput(alice1)); + await alice1.inputText(newGroupName, new EditGroupNameInput(alice1)); // Click done/apply - await alice1.clickOnElementById('update-group-info-confirm-button'); + await alice1.clickOnElementAll(new SaveGroupNameChangeButton(alice1)); await alice1.navigateBack(); // Check control message for changed name await alice1.waitForControlMessageToBePresent( diff --git a/run/test/specs/group_tests_edit_group_banner.spec.ts b/run/test/specs/group_tests_edit_group_banner.spec.ts index a999e136..cd11d5e5 100644 --- a/run/test/specs/group_tests_edit_group_banner.spec.ts +++ b/run/test/specs/group_tests_edit_group_banner.spec.ts @@ -1,8 +1,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { ConversationSettings } from './locators/conversation'; -import { EditGroup } from './locators'; -import { LatestReleaseBanner } from './locators/groups'; +import { LatestReleaseBanner, ManageMembersMenuItem } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; bothPlatformsIt({ @@ -24,7 +23,7 @@ async function editGroupBanner(platform: SupportedPlatformsType) { }); // Navigate to Edit Group screen await alice1.clickOnElementAll(new ConversationSettings(alice1)); - await alice1.clickOnElementAll(new EditGroup(alice1)); + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); const groupsBanner = await alice1.doesElementExist(new LatestReleaseBanner(alice1)); if (!groupsBanner) { throw new Error('v2 groups warning banner is not shown or text is incorrect'); diff --git a/run/test/specs/group_tests_invite_contact_banner.spec.ts b/run/test/specs/group_tests_invite_contact_banner.spec.ts index f04b5ad9..6a0d485e 100644 --- a/run/test/specs/group_tests_invite_contact_banner.spec.ts +++ b/run/test/specs/group_tests_invite_contact_banner.spec.ts @@ -1,8 +1,8 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { ConversationSettings } from './locators/conversation'; -import { EditGroup, InviteContactsButton } from './locators'; -import { LatestReleaseBanner } from './locators/groups'; +import { InviteContactsButton } from './locators'; +import { LatestReleaseBanner, ManageMembersMenuItem } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; bothPlatformsIt({ @@ -24,7 +24,7 @@ async function inviteContactGroupBanner(platform: SupportedPlatformsType) { }); // Navigate to Invite Contacts screen await alice1.clickOnElementAll(new ConversationSettings(alice1)); - await alice1.clickOnElementAll(new EditGroup(alice1)); + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); await alice1.clickOnElementAll(new InviteContactsButton(alice1)); const groupsBanner = await alice1.doesElementExist(new LatestReleaseBanner(alice1)); if (!groupsBanner) { diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index d8713d7f..5bc52c7a 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -1,11 +1,11 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { EditGroup } from './locators'; import { ConversationSettings, MessageInput } from './locators/conversation'; import { ConfirmRemovalButton, GroupMember, + ManageMembersMenuItem, MemberStatus, RemoveMemberButton, } from './locators/groups'; @@ -30,7 +30,7 @@ async function kickMember(platform: SupportedPlatformsType) { focusGroupConvo: true, }); await alice1.clickOnElementAll(new ConversationSettings(alice1)); - await alice1.clickOnElementAll(new EditGroup(alice1)); + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); await alice1.clickOnElementAll({ ...new GroupMember(alice1).build(USERNAME.BOB) }); await alice1.clickOnElementAll(new RemoveMemberButton(alice1)); await alice1.checkModalStrings( @@ -46,7 +46,7 @@ async function kickMember(platform: SupportedPlatformsType) { maxWait: 10000, }); await alice1.navigateBack(); - await alice1.onIOS().navigateBack(); + await alice1.navigateBack(); await Promise.all([ alice1.waitForControlMessageToBePresent( englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 13d80956..aaa779cb 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -28,15 +28,14 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType) await alice1.clickOnElementAll(new BlockUser(alice1)); await alice1.checkModalStrings( englishStrippedStr('block').toString(), - englishStrippedStr('blockDescription').withArgs({ name: bob.userName }).toString(), - true + englishStrippedStr('blockDescription').withArgs({ name: bob.userName }).toString() ); // Confirm block option await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); // On ios there is an alert that confirms that the user has been blocked await sleepFor(1000); // On ios, you need to navigate back to conversation screen to confirm block - await alice1.onIOS().navigateBack(); + await alice1.navigateBack(); // Look for alert at top of screen (Bob is blocked. Unblock them?) // Check device 1 for blocked status const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index c5c4ebb2..7e1d5b23 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -1,11 +1,13 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { EditGroup, EditGroupName } from './locators'; import { ConversationHeaderName, ConversationSettings } from './locators/conversation'; -import { EditGroupNameInput } from './locators/groups'; +import { + EditGroupNameInput, + UpdateGroupInformation, + SaveGroupNameChangeButton, +} from './locators/groups'; import { ConversationItem } from './locators/home'; -import { SaveNameChangeButton } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; @@ -43,7 +45,7 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { // Edit group await sleepFor(100); // click on group name to change it - await device1.clickOnElementAll(new EditGroupName(device1)); + await device1.clickOnElementAll(new UpdateGroupInformation(device1, testGroupName)); // Check new dialog await device1.checkModalStrings( englishStrippedStr(`updateGroupInformation`).toString(), @@ -54,7 +56,7 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { // Type in new group name await device1.inputText(newGroupName, new EditGroupNameInput(device1)); // Save changes - await device1.clickOnElementAll(new SaveNameChangeButton(device1)); + await device1.clickOnElementAll(new SaveGroupNameChangeButton(device1)); // Go back to conversation await device1.navigateBack(); // Check control message for changed name @@ -96,7 +98,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType) { await device1.clickOnElementAll(new ConversationSettings(device1)); // Click on Edit group option await sleepFor(1000); - await device1.clickOnElementAll(new EditGroup(device1)); + await device1.clickOnElementAll(new UpdateGroupInformation(device1)); // Click on current group name await device1.clickOnElementAll(new EditGroupNameInput(device1)); // Remove current group name @@ -105,7 +107,7 @@ async function linkedGroupAndroid(platform: SupportedPlatformsType) { await device1.clickOnElementAll(new EditGroupNameInput(device1)); await device1.inputText(newGroupName, new EditGroupNameInput(device1)); // Click done/apply - await device1.clickOnByAccessibilityID('Confirm'); + await device1.clickOnElementAll(new SaveGroupNameChangeButton(device1)); await device1.navigateBack(); // Check control message for changed name const groupNameNew = englishStrippedStr('groupNameNew') diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 2a865cb1..53b89546 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -2,6 +2,8 @@ import { LocatorsInterface } from '.'; import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { StrategyExtractionObj } from '../../../types/testing'; import type { UserNameType } from '@session-foundation/qa-seeder'; +import { GROUPNAME } from '../../../types/testing'; +import { DeviceWrapper } from '../../../types/DeviceWrapper'; export class GroupNameInput extends LocatorsInterface { public build(): StrategyExtractionObj { @@ -54,35 +56,65 @@ export class InviteContactConfirm extends LocatorsInterface { } } -export class EditGroupName extends LocatorsInterface { +export class UpdateGroupInformation extends LocatorsInterface { + private groupName?: GROUPNAME; + + // Receives a group name argument so that one locator can handle all possible group names + constructor(device: DeviceWrapper, groupName?: GROUPNAME) { + super(device); + this.groupName = groupName; + } + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'group-name', + }; + case 'ios': { + const groupName = this.groupName; + if (!groupName) { + throw new Error('groupName must be provided for iOS'); + } return { strategy: 'accessibility id', - selector: 'Edit', + selector: groupName, + }; + } + } + } +} + +export class EditGroupNameInput extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'update-group-info-name-input', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Edit group name', + selector: 'Group name text field', } as const; } } } -export class EditGroupNameInput extends LocatorsInterface { +export class SaveGroupNameChangeButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Group name', + selector: 'update-group-info-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Group name text field', + selector: 'Save', } as const; } } @@ -94,8 +126,7 @@ export class LeaveGroupButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: `network.loki.messenger:id/title`, - text: 'Leave group', + selector: 'leave-group-menu-option', } as const; case 'ios': return { @@ -105,13 +136,20 @@ export class LeaveGroupButton extends LocatorsInterface { } } } - export class LeaveGroupConfirm extends LocatorsInterface { public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Leave', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Leave', // SES-4022 + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Leave', + } as const; + } } } export class LatestReleaseBanner extends LocatorsInterface { @@ -257,4 +295,4 @@ export class ManageMembersMenuItem extends LocatorsInterface { }; } } -} \ No newline at end of file +} diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 6872be02..44518827 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -61,40 +61,6 @@ export class ApplyChanges extends LocatorsInterface { } } -export class EditGroup extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'edit-profile-icon' - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Edit group', - } as const; - } - } -} - -export class EditGroupName extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Username', - } as const; - case 'android': - return { - strategy: 'id', - selector: 'update-group-info-name-input', - } as const; - } - } -} - export class ReadReceiptsButton extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index 68fc39d4..8f6acb6b 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -90,8 +90,9 @@ async function sendCommunityInviteMessageAndroid(platform: SupportedPlatformsTyp await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); await alice1.clickOnElementAll(new GroupMember(alice1).build(bob.userName)); await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'invite-contacts-button'}); + strategy: 'id', + selector: 'invite-contacts-button', + }); // Check device 2 for invitation from user A await bob1.waitForTextElementToBePresent({ strategy: 'id', diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index 1e1eabcd..b10d8cc6 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -40,8 +40,8 @@ async function declineRequest(platform: SupportedPlatformsType) { await sleepFor(3000); await device2.checkModalStrings( englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsDelete').toString(), - true + englishStrippedStr('messageRequestsContactDelete').toString(), + false ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", @@ -58,7 +58,7 @@ async function declineRequest(platform: SupportedPlatformsType) { ]); // Navigate back to home page await sleepFor(100); - await device2.navigateBack(); + await device2.navigateBack(false); // Look for new conversation button to make sure it all worked await device2.waitForTextElementToBePresent(new PlusButton(device2)); // Close app diff --git a/run/test/specs/user_actions_delete_conversation.spec.ts b/run/test/specs/user_actions_delete_conversation.spec.ts index cc00c78c..683caf57 100644 --- a/run/test/specs/user_actions_delete_conversation.spec.ts +++ b/run/test/specs/user_actions_delete_conversation.spec.ts @@ -43,7 +43,7 @@ async function deleteConversation(platform: SupportedPlatformsType) { englishStrippedStr('conversationsDeleteDescription') .withArgs({ name: USERNAME.BOB }) .toString(), - true + false ); await alice1.clickOnElementAll(new DeleteContactModalConfirm(alice1)); await Promise.all([ 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 fd450dbf..68fcc40a 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -8,12 +8,12 @@ import { SupportedPlatformsType } from './utils/open_app'; import { testImage } from '../../constants/testfiles'; import { handlePhotosFirstTimeOpen } from './utils/handle_first_open'; -// TODO investigate why the Android Photos app throws an unexpected error when sharing +// TODO investigate why the Android Photos app throws an unexpected error when sharing bothPlatformsItSeparate({ title: 'Share to session', risk: 'low', ios: { - testCb: shareToSession, + testCb: shareToSession, }, android: { testCb: shareToSession, diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 5aecea7b..599cf28a 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -11,46 +11,46 @@ import test from '@playwright/test'; export const newUser = async (device: DeviceWrapper, userName: UserNameType): Promise => { return await test.step(`Create new user: ${userName}`, async () => { - let accountID = '' - let recoveryPhrase = ''; - await test.step('Tap Create Account button', async () => { - await device.clickOnElementAll(new CreateAccountButton(device)); - }); - await test.step('Enter Display Name', async () => { - await device.inputText(userName, new DisplayNameInput(device)); - await device.clickOnElementAll(new ContinueButton(device)); + let accountID = ''; + let recoveryPhrase = ''; + await test.step('Tap Create Account button', async () => { + await device.clickOnElementAll(new CreateAccountButton(device)); + }); + await test.step('Enter Display Name', async () => { + await device.inputText(userName, new DisplayNameInput(device)); + await device.clickOnElementAll(new ContinueButton(device)); + }); + await test.step('Choose Slow Mode for Notifications', async () => { + // Want to choose 'Slow Mode' so notifications don't interrupt test + await device.clickOnElementAll(new SlowModeRadio(device)); + await device.clickOnElementAll(new ContinueButton(device)); + }); + // TODO need to retry check every 1s for 5s + await test.step('Handle permission prompt if visible', async () => { + console.warn('about to look for Allow permission in 5s'); + await sleepFor(5000); + await device.checkPermissions('Allow'); + console.warn('looked for Allow permission'); + await sleepFor(1000); + }); + await test.step(`Save user's recovery password`, async () => { + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); + await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + //Save recovery password + const recoveryPhraseContainer = await device.clickOnElementAll( + new RecoveryPhraseContainer(device) + ); + await device.onAndroid().clickOnElementAll(new CopyButton(device)); + // Save recovery phrase as variable + recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); + console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); + await device.navigateBack(false); + }); + await test.step(`Save user's Account ID`, async () => { + await device.clickOnElementAll(new UserSettings(device)); + accountID = await device.grabTextFromAccessibilityId('Account ID'); + await device.closeScreen(false); + }); + return { userName, accountID, recoveryPhrase }; }); - await test.step('Choose Slow Mode for Notifications', async () => { - // Want to choose 'Slow Mode' so notifications don't interrupt test - await device.clickOnElementAll(new SlowModeRadio(device)); - await device.clickOnElementAll(new ContinueButton(device)); - }); - // TODO need to retry check every 1s for 5s - await test.step('Handle permission prompt if visible', async () => { - console.warn('about to look for Allow permission in 5s'); - await sleepFor(5000); - await device.checkPermissions('Allow'); - console.warn('looked for Allow permission'); - await sleepFor(1000); - }); - await test.step(`Save user's recovery password`, async () => { - await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); - await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); - //Save recovery password - const recoveryPhraseContainer = await device.clickOnElementAll( - new RecoveryPhraseContainer(device) - ); - await device.onAndroid().clickOnElementAll(new CopyButton(device)); - // Save recovery phrase as variable - recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); - console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); - await device.navigateBack(false); - }); - await test.step(`Save user's Account ID`, async () => { - await device.clickOnElementAll(new UserSettings(device)); - accountID = await device.grabTextFromAccessibilityId('Account ID'); - await device.closeScreen(false); - }); - return { userName, accountID, recoveryPhrase }; -}); -}; \ No newline at end of file +}; diff --git a/run/test/specs/utils/create_group.ts b/run/test/specs/utils/create_group.ts index 6b0271ec..a2a29b57 100644 --- a/run/test/specs/utils/create_group.ts +++ b/run/test/specs/utils/create_group.ts @@ -8,6 +8,7 @@ import { CreateGroupOption } from '../locators/start_conversation'; import { newContact } from './create_contact'; import { sortByPubkey } from './get_account_id'; import { SupportedPlatformsType } from './open_app'; +import { sleepFor } from './sleep_for'; export const createGroup = async ( platform: SupportedPlatformsType, @@ -46,8 +47,12 @@ export const createGroup = async ( await device1.clickOnElementAll({ ...new Contact(device1).build(), text: userThree.userName }); // Select tick await device1.clickOnElementAll(new CreateGroupButton(device1)); + await sleepFor(3000); // Enter group chat on device 2 and 3 - await Promise.all([device2.onAndroid().navigateBack(), device3.onAndroid().navigateBack()]); + await Promise.all([ + device2.onAndroid().navigateBack(false), + device3.onAndroid().navigateBack(false), + ]); await Promise.all([ device2.clickOnElementAll(new ConversationItem(device2, group.userName)), device3.clickOnElementAll(new ConversationItem(device3, group.userName)), diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index ef069969..48401eb2 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -7,10 +7,7 @@ import { PlusButton } from '../locators/home'; import test from '@playwright/test'; export const restoreAccount = async (device: DeviceWrapper, user: User) => { - await device.clickOnElementAll({ - strategy: 'accessibility id', - selector: 'Restore your session button', - }); + await device.clickOnElementAll(new AccountRestoreButton(device)); await device.inputText(user.recoveryPhrase, new SeedPhraseInput(device)); // Wait for continue button to become active await sleepFor(500); @@ -69,22 +66,22 @@ export const restoreAccountNoFallback = async (device: DeviceWrapper, recoveryPh const displayName = await device.doesElementExist({ strategy: 'accessibility id', selector: 'Enter display name', - maxWait: 1000, + maxWait: 2000, }); if (displayName) { throw new Error('Account not found'); } console.info('Display name found: Loading account'); - // Wait for permissions modal to pop up - await sleepFor(500); - await device.checkPermissions('Allow'); - await sleepFor(1000); - await device.hasElementBeenDeleted({ - ...new ContinueButton(device).build(), - maxWait: 1000, + // Wait for permissions modal to pop up + await sleepFor(500); + await device.checkPermissions('Allow'); + await sleepFor(1000); + await device.hasElementBeenDeleted({ + ...new ContinueButton(device).build(), + maxWait: 1000, + }); + // Check that button was clicked + await device.waitForTextElementToBePresent(new PlusButton(device)); }); - // Check that button was clicked - await device.waitForTextElementToBePresent(new PlusButton(device)); -}); -}; \ No newline at end of file +}; diff --git a/run/types/testing.ts b/run/types/testing.ts index 68c29632..da705a62 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -365,7 +365,8 @@ export type AccessibilityId = | 'MeetingSE' | 'Donate' | 'blocked-banner' - | 'Manage Members'; + | 'Manage Members' + | `${GROUPNAME}`; export type Id = | 'Modal heading' @@ -480,7 +481,10 @@ export type Id = | 'delete-for-everyone' | 'edit-profile-icon' | 'update-group-info-confirm-button' - | 'update-group-info-name-input'; + | 'update-group-info-name-input' + | 'group-name' + | 'leave-group-menu-option' + | 'Leave'; export type TestRisk = 'high' | 'medium' | 'low'; From 8da4e104b65ad8b9d08e1e0c2a2cc838fd4d7c45 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Jun 2025 18:47:56 +1000 Subject: [PATCH 22/60] revert test.step changes to create_account --- run/test/specs/utils/create_account.ts | 78 +++++++++++--------------- 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 599cf28a..15fa9c1e 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -7,50 +7,40 @@ import { CreateAccountButton, DisplayNameInput, SlowModeRadio } from '../locator import { UserSettings } from '../locators/settings'; import { ContinueButton } from '../locators/global'; import { CopyButton } from '../locators/start_conversation'; -import test from '@playwright/test'; export const newUser = async (device: DeviceWrapper, userName: UserNameType): Promise => { - return await test.step(`Create new user: ${userName}`, async () => { - let accountID = ''; - let recoveryPhrase = ''; - await test.step('Tap Create Account button', async () => { - await device.clickOnElementAll(new CreateAccountButton(device)); - }); - await test.step('Enter Display Name', async () => { - await device.inputText(userName, new DisplayNameInput(device)); - await device.clickOnElementAll(new ContinueButton(device)); - }); - await test.step('Choose Slow Mode for Notifications', async () => { - // Want to choose 'Slow Mode' so notifications don't interrupt test - await device.clickOnElementAll(new SlowModeRadio(device)); - await device.clickOnElementAll(new ContinueButton(device)); - }); - // TODO need to retry check every 1s for 5s - await test.step('Handle permission prompt if visible', async () => { - console.warn('about to look for Allow permission in 5s'); - await sleepFor(5000); - await device.checkPermissions('Allow'); - console.warn('looked for Allow permission'); - await sleepFor(1000); - }); - await test.step(`Save user's recovery password`, async () => { - await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); - await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); - //Save recovery password - const recoveryPhraseContainer = await device.clickOnElementAll( - new RecoveryPhraseContainer(device) - ); - await device.onAndroid().clickOnElementAll(new CopyButton(device)); - // Save recovery phrase as variable - recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); - console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); - await device.navigateBack(false); - }); - await test.step(`Save user's Account ID`, async () => { - await device.clickOnElementAll(new UserSettings(device)); - accountID = await device.grabTextFromAccessibilityId('Account ID'); - await device.closeScreen(false); - }); - return { userName, accountID, recoveryPhrase }; - }); + // Click create session ID + await device.clickOnElementAll(new CreateAccountButton(device)); + // Input username + await device.inputText(userName, new DisplayNameInput(device)); + // Click continue + await device.clickOnElementAll(new ContinueButton(device)); + // Choose message notification options + // Want to choose 'Slow Mode' so notifications don't interrupt test + await device.clickOnElementAll(new SlowModeRadio(device)); + // Select Continue to save notification settings + await device.clickOnElementAll(new ContinueButton(device)); + // TODO need to retry check every 1s for 5s + console.warn('about to look for Allow permission in 5s'); + await sleepFor(5000); + await device.checkPermissions('Allow'); + console.warn('looked for Allow permission'); + await sleepFor(1000); + // Click on 'continue' button to open recovery phrase modal + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); + await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + //Save recovery password + const recoveryPhraseContainer = await device.clickOnElementAll( + new RecoveryPhraseContainer(device) + ); + await device.onAndroid().clickOnElementAll(new CopyButton(device)); + // Save recovery phrase as variable + const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); + console.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); + // Exit Modal + await device.navigateBack(false); + await device.clickOnElementAll(new UserSettings(device)); + const accountID = await device.grabTextFromAccessibilityId('Account ID'); + await device.closeScreen(false); + return { userName, accountID, recoveryPhrase }; }; From e7f4460a20c6fd1eb7d8ea7445d86b81430c87d3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 09:29:03 +1000 Subject: [PATCH 23/60] fix: Appearance is now id-based --- run/test/specs/locators/settings.ts | 4 ++++ run/types/testing.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index cb61bc75..a520de9c 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -181,6 +181,10 @@ export class AppearanceMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'Appearance', + } as const; case 'ios': return { strategy: 'accessibility id', diff --git a/run/types/testing.ts b/run/types/testing.ts index da705a62..8e5dae0b 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -484,7 +484,8 @@ export type Id = | 'update-group-info-name-input' | 'group-name' | 'leave-group-menu-option' - | 'Leave'; + | 'Leave' + | 'Appearance'; export type TestRisk = 'high' | 'medium' | 'low'; From 1bd6e993d8b0c7b1b75baa02576aef78ce75d32e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 10:58:52 +1000 Subject: [PATCH 24/60] fix: CI workflow starts emulators with no snapshot --- .github/workflows/android-regression.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index b697d469..f529461f 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -145,7 +145,7 @@ jobs: shell: bash run: | source ./scripts/ci.sh - start_with_snapshots + start_for_snapshots - name: List tests part of this run uses: ./github/actions/list-tests From b0a2a2771c06aa7dd49d85a4767d442db92c15c7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 11:31:08 +1000 Subject: [PATCH 25/60] fix: Android 1.25.0 changes --- run/test/specs/app_disguise_set.spec.ts | 2 +- run/test/specs/group_tests_add_contact.spec.ts | 10 ++++++---- run/test/specs/group_tests_leave_group.spec.ts | 8 ++++++-- run/test/specs/linked_group_leave.spec.ts | 12 +++++++++--- run/test/specs/locators/groups.ts | 4 ++-- run/test/specs/locators/index.ts | 18 ------------------ 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index cdb37059..275763f8 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -43,7 +43,7 @@ async function appDisguiseSetIcon(platform: SupportedPlatformsType) { ); await device.clickOnElementAll(new CloseAppButton(device)); await sleepFor(2000); - // // Open app library and check for disguised app + // Open app library and check for disguised app await device.swipeFromBottom(); await device.waitForTextElementToBePresent(new DisguisedApp(device)); } finally { diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 7f3ee5e3..760e0b3a 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -48,12 +48,11 @@ async function addContactToGroup(platform: SupportedPlatformsType) { ...new Contact(alice1).build(), text: USERNAME.DRACULA, }); - // Click done/apply await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); - // Click done/apply again + // Leave Manage Members + await alice1.navigateBack(); + // Leave Conversation Settings await alice1.navigateBack(); - // iOS doesn't automatically go back to conversation settings - await alice1.onIOS().navigateBack(); // Check control messages await Promise.all( [alice1, bob1, charlie1].map(device => @@ -62,6 +61,9 @@ async function addContactToGroup(platform: SupportedPlatformsType) { ) ) ); + // Leave conversation + await unknown1.navigateBack(); + // Leave Message Requests screen await unknown1.navigateBack(); await unknown1.selectByText('Conversation list item', group.groupName); // Check for control message on device 4 diff --git a/run/test/specs/group_tests_leave_group.spec.ts b/run/test/specs/group_tests_leave_group.spec.ts index 225538e8..2f2cb895 100644 --- a/run/test/specs/group_tests_leave_group.spec.ts +++ b/run/test/specs/group_tests_leave_group.spec.ts @@ -1,7 +1,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationSettings } from './locators/conversation'; -import { LeaveGroupButton, LeaveGroupConfirm } from './locators/groups'; +import { LeaveGroupMenuItem, LeaveGroupConfirm } from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { sleepFor } from './utils/index'; @@ -27,8 +27,12 @@ async function leaveGroup(platform: SupportedPlatformsType) { }); await charlie1.clickOnElementAll(new ConversationSettings(charlie1)); await sleepFor(1000); - await charlie1.clickOnElementAll(new LeaveGroupButton(charlie1)); + await charlie1.clickOnElementAll(new LeaveGroupMenuItem(charlie1)); // Modal with Leave/Cancel + await charlie1.checkModalStrings( + englishStrippedStr('groupLeave').toString(), + englishStrippedStr('groupLeaveDescription').withArgs({ group_name: testGroupName }).toString() + ); await charlie1.clickOnElementAll(new LeaveGroupConfirm(charlie1)); // Check for control message const groupMemberLeft = englishStrippedStr('groupMemberLeft') diff --git a/run/test/specs/linked_group_leave.spec.ts b/run/test/specs/linked_group_leave.spec.ts index ae90af1b..5f96a35a 100644 --- a/run/test/specs/linked_group_leave.spec.ts +++ b/run/test/specs/linked_group_leave.spec.ts @@ -1,13 +1,13 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { LeaveGroup } from './locators'; import { ConversationSettings } from './locators/conversation'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; import { linkedDevice } from './utils/link_device'; import { SupportedPlatformsType, closeApp, openAppFourDevices } from './utils/open_app'; +import { LeaveGroupMenuItem, LeaveGroupConfirm } from './locators/groups'; bothPlatformsIt({ title: 'Leave group linked device', @@ -30,8 +30,14 @@ async function leaveGroupLinkedDevice(platform: SupportedPlatformsType) { await sleepFor(1000); await device3.clickOnElementAll(new ConversationSettings(device3)); await sleepFor(1000); - await device3.clickOnElementAll(new LeaveGroup(device3)); - await device3.clickOnByAccessibilityID('Leave'); + await device3.clickOnElementAll(new LeaveGroupMenuItem(device3)); + await device3.checkModalStrings( + englishStrippedStr('groupLeave').toString(), + englishStrippedStr('groupLeaveDescription').withArgs({ group_name: testGroupName }).toString() + ); + // Modal with Leave/Cancel + await device3.clickOnElementAll(new LeaveGroupConfirm(device3)); + // Check for control message // Check for control message await sleepFor(5000); await device4.onIOS().hasTextElementBeenDeleted('Conversation list item', testGroupName); diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 53b89546..255f61a0 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -45,7 +45,7 @@ export class InviteContactConfirm extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Confirm invite button', + selector: 'invite-contacts-button', } as const; case 'ios': return { @@ -120,7 +120,7 @@ export class SaveGroupNameChangeButton extends LocatorsInterface { } } -export class LeaveGroupButton extends LocatorsInterface { +export class LeaveGroupMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 44518827..2c62fab3 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -330,24 +330,6 @@ export class DeleteMessageConfirmationModal extends LocatorsInterface { } } -export class LeaveGroup extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: `network.loki.messenger:id/title`, - text: 'Leave group', - }; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Leave group', - }; - } - } -} - export class BlockUserConfirmationModal extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { From e5370810f21964d4f9145c841b66c225687949ff Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 11:36:31 +1000 Subject: [PATCH 26/60] fix: update app_disguise.png --- run/screenshots/android/app_disguise.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/screenshots/android/app_disguise.png b/run/screenshots/android/app_disguise.png index 95cf431e..6349657d 100644 --- a/run/screenshots/android/app_disguise.png +++ b/run/screenshots/android/app_disguise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18ed347a459550771454d6038a4a4eb2d52792f0392ece8e8dace5c919251007 -size 127457 +oid sha256:c979e249014dc0c7083020ef91d922e44a63aace080176c8551f40e6ccf8b223 +size 125751 From 4ca9ef90eaa68d1f7bb23c2a4eefeebd142ae7fe Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 11:57:54 +1000 Subject: [PATCH 27/60] fix: try to start emulators without snapshot, wait for them to boot --- .github/workflows/android-regression.yml | 6 +++--- scripts/ci.sh | 26 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index f529461f..590672b5 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -141,11 +141,11 @@ jobs: adb kill-server; adb start-server; - - name: Start emulators - shell: bash + - name: Start emulators (clean boot) run: | source ./scripts/ci.sh - start_for_snapshots + start_without_snapshots + wait_for_emulators - name: List tests part of this run uses: ./github/actions/list-tests diff --git a/scripts/ci.sh b/scripts/ci.sh index dac3a18c..5eafeec3 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -97,3 +97,29 @@ function start_with_snapshots() { sleep 5 done } + +function start_without_snapshots() { + for i in {1..4} + do + DISPLAY=:0 emulator @emulator$i \ + -gpu host \ + -no-snapshot \ + -accel on & + sleep 20 + done +} + +function wait_for_emulators() { + for port in 5554 5556 5558 5560 + do + for i in {1..60}; do + if adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then + echo "emulator-$port booted" + break + else + echo "Waiting for emulator-$port to boot..." + sleep 5 + fi + done + done +} \ No newline at end of file From df68e143b6b1ca92f6c199e2b70606a68510141c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 14:35:49 +1000 Subject: [PATCH 28/60] fix: stop taking full page browser screenshots because they are unreliable --- run/screenshots/android/browser_network_page.png | 3 --- run/screenshots/android/browser_staking_page.png | 3 --- run/test/specs/donate.spec.ts | 3 ++- run/test/specs/network_page_link_network.spec.ts | 6 +++--- run/test/specs/network_page_link_staking.spec.ts | 5 ++--- run/test/specs/onboarding_pp.spec.ts | 3 ++- run/test/specs/onboarding_tos.spec.ts | 3 ++- run/test/specs/utils/utilities.ts | 7 +++++++ 8 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 run/screenshots/android/browser_network_page.png delete mode 100644 run/screenshots/android/browser_staking_page.png diff --git a/run/screenshots/android/browser_network_page.png b/run/screenshots/android/browser_network_page.png deleted file mode 100644 index 85c46cfc..00000000 --- a/run/screenshots/android/browser_network_page.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ffd6f15561e8ecbc19b46d4b2eec142c6f5f456c4c58f57ce93f2aa10caa0cb7 -size 223397 diff --git a/run/screenshots/android/browser_staking_page.png b/run/screenshots/android/browser_staking_page.png deleted file mode 100644 index 4467fbe3..00000000 --- a/run/screenshots/android/browser_staking_page.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e1fee84c5fc3286b5ea28eb46311ecad44c2bf3c0189175aef8f1fddc48bf2f -size 182553 diff --git a/run/test/specs/donate.spec.ts b/run/test/specs/donate.spec.ts index 1a0ee158..f4aed0d2 100644 --- a/run/test/specs/donate.spec.ts +++ b/run/test/specs/donate.spec.ts @@ -6,7 +6,7 @@ import { DonationsMenuItem, UserSettings } from './locators/settings'; import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { ensureHttpsURL } from './utils/utilities'; +import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities'; import { OpenLinkButton } from './locators/network_page'; bothPlatformsIt({ @@ -45,6 +45,7 @@ async function donateLinkout(platform: SupportedPlatformsType) { `The retrieved URL does not match the expected. The retrieved URL is ${fullRetrievedURL}` ); } + await assertUrlIsReachable(linkURL); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/network_page_link_network.spec.ts b/run/test/specs/network_page_link_network.spec.ts index 5df5c9cf..5a4999b6 100644 --- a/run/test/specs/network_page_link_network.spec.ts +++ b/run/test/specs/network_page_link_network.spec.ts @@ -11,8 +11,7 @@ import { UserSettings } from './locators/settings'; import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { ensureHttpsURL } from './utils/utilities'; -import { verifyPageScreenshot } from './utils/verify_screenshots'; +import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities'; bothPlatformsIt({ title: 'Network page learn more network link', @@ -52,7 +51,8 @@ async function networkPageLearnMore(platform: SupportedPlatformsType) { } else { console.log('The URLs match.'); } - await verifyPageScreenshot(platform, device, 'network_page'); + await assertUrlIsReachable(linkURL); + // await verifyPageScreenshot(platform, device, 'network_page'); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/network_page_link_staking.spec.ts b/run/test/specs/network_page_link_staking.spec.ts index dadfc03c..05386f2a 100644 --- a/run/test/specs/network_page_link_staking.spec.ts +++ b/run/test/specs/network_page_link_staking.spec.ts @@ -1,4 +1,3 @@ -import { verifyPageScreenshot } from './utils/verify_screenshots'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; @@ -12,7 +11,7 @@ import { UserSettings } from './locators/settings'; import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { newUser } from './utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { ensureHttpsURL } from './utils/utilities'; +import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities'; bothPlatformsIt({ title: 'Network page learn more staking link', @@ -53,7 +52,7 @@ async function networkPageLearnMore(platform: SupportedPlatformsType) { } else { console.log('The URLs match.'); } - await verifyPageScreenshot(platform, device, 'staking_page'); + await assertUrlIsReachable(linkURL); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/onboarding_pp.spec.ts b/run/test/specs/onboarding_pp.spec.ts index 9e71e4cf..4c802acf 100644 --- a/run/test/specs/onboarding_pp.spec.ts +++ b/run/test/specs/onboarding_pp.spec.ts @@ -3,7 +3,7 @@ import { SafariAddressBar, URLInputField } from './locators/browsers'; import { PrivacyPolicyButton, SplashScreenLinks } from './locators/onboarding'; import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { ensureHttpsURL } from './utils/utilities'; +import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities'; bothPlatformsIt({ title: 'Onboarding privacy policy', @@ -37,6 +37,7 @@ async function onboardingPP(platform: SupportedPlatformsType) { `The retrieved URL does not match the expected. The retrieved URL is ${fullRetrievedURL}` ); } + await assertUrlIsReachable(ppURL); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/onboarding_tos.spec.ts b/run/test/specs/onboarding_tos.spec.ts index 6979f6d2..cb188cca 100644 --- a/run/test/specs/onboarding_tos.spec.ts +++ b/run/test/specs/onboarding_tos.spec.ts @@ -3,7 +3,7 @@ import { TermsOfServiceButton, SplashScreenLinks } from './locators/onboarding'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; import { handleChromeFirstTimeOpen } from './utils/handle_first_open'; import { URLInputField, SafariAddressBar } from './locators/browsers'; -import { ensureHttpsURL } from './utils/utilities'; +import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities'; bothPlatformsIt({ title: 'Onboarding terms of service', @@ -36,6 +36,7 @@ async function onboardingTOS(platform: SupportedPlatformsType) { `The retrieved URL does not match the expected. The retrieved URL is ${fullRetrievedURL}` ); } + await assertUrlIsReachable(tosURL); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/utils/utilities.ts b/run/test/specs/utils/utilities.ts index 6c41e628..a599fd96 100644 --- a/run/test/specs/utils/utilities.ts +++ b/run/test/specs/utils/utilities.ts @@ -107,3 +107,10 @@ export function getDiffDirectory() { fs.mkdirSync(diffsDir, { recursive: true }); return diffsDir; } + +export async function assertUrlIsReachable(url: string): Promise { + const response = await fetch(url); + if (response.status !== 200) { + throw new Error(`Expected status 200 but got ${response.status} for URL: ${url}`); + } +} \ No newline at end of file From 7b09e541af372883dc89512f791106eba9d8702f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 16:03:59 +1000 Subject: [PATCH 29/60] fix: wipe emulators on start --- scripts/ci.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci.sh b/scripts/ci.sh index 5eafeec3..a966215d 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -104,6 +104,7 @@ function start_without_snapshots() { DISPLAY=:0 emulator @emulator$i \ -gpu host \ -no-snapshot \ + -wipe-data \ -accel on & sleep 20 done From 5e16fd49ee081e70f30e75faf6bee5a570dda774 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 16:04:10 +1000 Subject: [PATCH 30/60] chore: linting --- run/test/specs/network_page_link_network.spec.ts | 1 - run/test/specs/utils/utilities.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/run/test/specs/network_page_link_network.spec.ts b/run/test/specs/network_page_link_network.spec.ts index 5a4999b6..1d605c47 100644 --- a/run/test/specs/network_page_link_network.spec.ts +++ b/run/test/specs/network_page_link_network.spec.ts @@ -52,7 +52,6 @@ async function networkPageLearnMore(platform: SupportedPlatformsType) { console.log('The URLs match.'); } await assertUrlIsReachable(linkURL); - // await verifyPageScreenshot(platform, device, 'network_page'); // Close browser and app await device.backToSession(); await closeApp(device); diff --git a/run/test/specs/utils/utilities.ts b/run/test/specs/utils/utilities.ts index a599fd96..ce0d1ecf 100644 --- a/run/test/specs/utils/utilities.ts +++ b/run/test/specs/utils/utilities.ts @@ -113,4 +113,4 @@ export async function assertUrlIsReachable(url: string): Promise { if (response.status !== 200) { throw new Error(`Expected status 200 but got ${response.status} for URL: ${url}`); } -} \ No newline at end of file +} From 13691883ae4076c2c0a2684de2cbd417f04ca089 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 16:37:04 +1000 Subject: [PATCH 31/60] fix: CI never finds test_video.mp4 --- run/types/DeviceWrapper.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 69a2d0d0..b4aac6b5 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1445,7 +1445,35 @@ export class DeviceWrapper { text: 'Allow', }); await sleepFor(2000); - await this.clickOnTextElementById('android:id/title', testVideo); + let videoElement = await this.doesElementExist({ + strategy: 'id', + selector: 'android:id/title', + text: testVideo, + maxWait: 5000, + }); + if (!videoElement) { + // Try to reveal the video element + await this.clickOnElementAll({ + strategy: 'class name', + selector: 'android.widget.Button', + text: 'Videos', + }); + // Try again to find the video element after revealing + videoElement = await this.doesElementExist({ + strategy: 'id', + selector: 'android:id/title', + text: testVideo, + }); + } + if (videoElement) { + await this.clickOnElementAll({ + strategy: 'id', + selector: 'android:id/title', + text: testVideo, + }); + } else { + throw new Error(`Video element with text "${testVideo}" not found after attempting to reveal it.`); + } await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), maxWait: 20000, From 8e5a67b3679a6b748ad90f2a6baa253d05745303 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 16:44:50 +1000 Subject: [PATCH 32/60] fix: CI never finds test_file.pdf --- run/types/DeviceWrapper.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index b4aac6b5..2df0edf8 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1472,7 +1472,9 @@ export class DeviceWrapper { text: testVideo, }); } else { - throw new Error(`Video element with text "${testVideo}" not found after attempting to reveal it.`); + throw new Error( + `Video element with text "${testVideo}" not found after attempting to reveal it.` + ); } await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), @@ -1531,6 +1533,37 @@ export class DeviceWrapper { text: 'Allow', }); await sleepFor(1000); + let documentElement = await this.doesElementExist({ + strategy: 'id', + selector: 'android:id/title', + text: testFile, + maxWait: 5000, + }); + if (!documentElement) { + // Try to reveal the video element + await this.clickOnElementAll({ + strategy: 'class name', + selector: 'android.widget.Button', + text: 'Documents', + }); + // Try again to find the video element after revealing + documentElement = await this.doesElementExist({ + strategy: 'id', + selector: 'android:id/title', + text: testFile, + }); + } + if (documentElement) { + await this.clickOnElementAll({ + strategy: 'id', + selector: 'android:id/title', + text: testFile, + }); + } else { + throw new Error( + `File element with text "${testFile}" not found after attempting to reveal it.` + ); + } await this.clickOnTextElementById('android:id/title', testFile); } // Checking Sent status on both platforms From daf3ab2a2fba25f21ee4faf21afc5d2acfbc89b6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 16:58:59 +1000 Subject: [PATCH 33/60] tidy up some comments --- run/types/DeviceWrapper.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2df0edf8..90473e8c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1451,14 +1451,15 @@ export class DeviceWrapper { text: testVideo, maxWait: 5000, }); + // This codepath is purely for the CI if (!videoElement) { - // Try to reveal the video element + // Try to reveal the video by selecting/filtering Videos in the native UI await this.clickOnElementAll({ strategy: 'class name', selector: 'android.widget.Button', text: 'Videos', }); - // Try again to find the video element after revealing + // Try again to find the video file after filtering videoElement = await this.doesElementExist({ strategy: 'id', selector: 'android:id/title', @@ -1473,7 +1474,7 @@ export class DeviceWrapper { }); } else { throw new Error( - `Video element with text "${testVideo}" not found after attempting to reveal it.` + `Video "${testVideo}" not found after attempting to reveal it.` ); } await this.waitForTextElementToBePresent({ @@ -1539,14 +1540,15 @@ export class DeviceWrapper { text: testFile, maxWait: 5000, }); + // This codepath is purely for the CI if (!documentElement) { - // Try to reveal the video element + // Try to reveal the pdf by selecting/filtering Documents in the native UI await this.clickOnElementAll({ strategy: 'class name', selector: 'android.widget.Button', text: 'Documents', }); - // Try again to find the video element after revealing + // Try again to find the pdf file after revealing documentElement = await this.doesElementExist({ strategy: 'id', selector: 'android:id/title', @@ -1561,7 +1563,7 @@ export class DeviceWrapper { }); } else { throw new Error( - `File element with text "${testFile}" not found after attempting to reveal it.` + `File "${testFile}" not found after attempting to reveal it.` ); } await this.clickOnTextElementById('android:id/title', testFile); From 6e69f9f308842f4a022a0f4078d1cc585aeda834 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Jun 2025 17:12:13 +1000 Subject: [PATCH 34/60] wip: try to add screenshot diffs to allure --- run/test/specs/utils/verify_screenshots.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 6f316ac3..dc26cdfb 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -8,6 +8,8 @@ import { LocatorsInterfaceScreenshot } from '../locators'; import { SupportedPlatformsType } from './open_app'; import { BrowserPageScreenshot } from './screenshot_paths'; import { cropScreenshot, getDiffDirectory, saveImage } from './utilities'; +import { allure } from 'allure-playwright' + // The function takes a screenshot of an element and verifies it against a baseline screenshot // Supports locators with optional multiple states, enforcing correct state usage where applicable @@ -54,6 +56,11 @@ export async function verifyElementScreenshot< if (!equal) { const diffImagePath = path.join(diffsDir, `${uuid}_diffImage.png`); await diffImage.save(diffImagePath); + void allure.attachment( + 'Screenshot Diff', + fs.readFileSync(diffImagePath), + 'image/png' + ); throw new Error(`The images do not match. The diff has been saved to ${diffImagePath}`); } // Cleanup of element screenshot file on success From 406fefb9f60f4b23fbf9aed3e756ca1c81e270aa Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 11:33:17 +1000 Subject: [PATCH 35/60] feat: allure report includes visual diffs --- .gitignore | 2 +- eslint.config.mjs | 1 + .../android/landingpage_new_account.png | 4 +- run/test/specs/app_disguise_icons.spec.ts | 5 +- .../specs/landing_page_new_account.spec.ts | 11 +- .../landing_page_restore_account.spec.ts | 5 +- run/test/specs/utils/verify_screenshots.ts | 105 +++++++++++++----- 7 files changed, 98 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index aba5253a..f4b3a32d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ avd/* .eslintcache test-results.csv *.csv -/allure \ No newline at end of file +/allure* \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index ebef391c..c8f00ce5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ export default tseslint.config( 'run/**/*.js', 'scripts/*.js', 'avd/', + 'allure*/', ], }, eslint.configs.recommended, diff --git a/run/screenshots/android/landingpage_new_account.png b/run/screenshots/android/landingpage_new_account.png index 03808de9..686801d9 100644 --- a/run/screenshots/android/landingpage_new_account.png +++ b/run/screenshots/android/landingpage_new_account.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67df5e6fbc1b9c3be58275db41b3c6bc64911c0755e38196082062a585df457b -size 81721 +oid sha256:c2171f984e0476ac215b46aa47bda90392db1cea327cb1c7f375911454839eca +size 125515 diff --git a/run/test/specs/app_disguise_icons.spec.ts b/run/test/specs/app_disguise_icons.spec.ts index 538fc98c..296d1c2b 100644 --- a/run/test/specs/app_disguise_icons.spec.ts +++ b/run/test/specs/app_disguise_icons.spec.ts @@ -6,6 +6,7 @@ import { AppearanceMenuItem, SelectAppIcon, UserSettings } from './locators/sett import { verifyElementScreenshot } from './utils/verify_screenshots'; import { AppDisguisePageScreenshot } from './utils/screenshot_paths'; import { sleepFor } from './utils'; +import type { TestInfo } from '@playwright/test'; bothPlatformsIt({ title: 'App disguise icons', @@ -14,7 +15,7 @@ bothPlatformsIt({ testCb: appDisguiseIcons, }); -async function appDisguiseIcons(platform: SupportedPlatformsType) { +async function appDisguiseIcons(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform); await newUser(device, USERNAME.ALICE); await device.clickOnElementAll(new UserSettings(device)); @@ -25,6 +26,6 @@ async function appDisguiseIcons(platform: SupportedPlatformsType) { // Must scroll down to reveal the app disguise option await device.scrollDown(); await device.clickOnElementAll(new SelectAppIcon(device)); - await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device)); + await verifyElementScreenshot(device, new AppDisguisePageScreenshot(device), testInfo); await closeApp(device); } diff --git a/run/test/specs/landing_page_new_account.spec.ts b/run/test/specs/landing_page_new_account.spec.ts index 9b726a71..36edcf9f 100644 --- a/run/test/specs/landing_page_new_account.spec.ts +++ b/run/test/specs/landing_page_new_account.spec.ts @@ -4,6 +4,7 @@ import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from import { USERNAME } from '../../types/testing'; import { verifyElementScreenshot } from './utils/verify_screenshots'; import { EmptyLandingPageScreenshot } from './utils/screenshot_paths'; +import type { TestInfo } from '@playwright/test'; bothPlatformsIt({ title: 'Landing page new account', @@ -11,11 +12,15 @@ bothPlatformsIt({ testCb: landingPageNewAccount, countOfDevicesNeeded: 1, }); - -async function landingPageNewAccount(platform: SupportedPlatformsType) { +async function landingPageNewAccount(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform); await newUser(device, USERNAME.ALICE); // Verify that the party popper is shown on the landing page - await verifyElementScreenshot(device, new EmptyLandingPageScreenshot(device), 'new_account'); + await verifyElementScreenshot( + device, + new EmptyLandingPageScreenshot(device), + testInfo, + 'new_account' + ); await closeApp(device); } diff --git a/run/test/specs/landing_page_restore_account.spec.ts b/run/test/specs/landing_page_restore_account.spec.ts index e2d07611..8aa2c4eb 100644 --- a/run/test/specs/landing_page_restore_account.spec.ts +++ b/run/test/specs/landing_page_restore_account.spec.ts @@ -4,6 +4,7 @@ import { verifyElementScreenshot } from './utils/verify_screenshots'; import { EmptyLandingPageScreenshot } from './utils/screenshot_paths'; import { USERNAME } from '@session-foundation/qa-seeder'; import { linkedDevice } from './utils/link_device'; +import type { TestInfo } from '@playwright/test'; bothPlatformsIt({ title: 'Landing page restore account', @@ -12,11 +13,11 @@ bothPlatformsIt({ countOfDevicesNeeded: 2, }); -async function landingPageRestoreAccount(platform: SupportedPlatformsType) { +async function landingPageRestoreAccount(platform: SupportedPlatformsType, testInfo: TestInfo) { // Creating a linked device is used as a shortcut to restore an account const { device1: alice1, device2: alice2 } = await openAppTwoDevices(platform); await linkedDevice(alice1, alice2, USERNAME.ALICE); // Verify that the Session logo is shown on the landing page - await verifyElementScreenshot(alice2, new EmptyLandingPageScreenshot(alice2), 'restore_account'); + await verifyElementScreenshot(alice2, new EmptyLandingPageScreenshot(alice2), testInfo, 'restore_account'); await closeApp(alice1, alice2); } diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index dc26cdfb..8a169616 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -8,25 +8,46 @@ import { LocatorsInterfaceScreenshot } from '../locators'; import { SupportedPlatformsType } from './open_app'; import { BrowserPageScreenshot } from './screenshot_paths'; import { cropScreenshot, getDiffDirectory, saveImage } from './utilities'; -import { allure } from 'allure-playwright' +import { TestInfo } from '@playwright/test'; +type Attachment = { + name: string; + body: Buffer | string; + contentType: string; +}; -// The function takes a screenshot of an element and verifies it against a baseline screenshot -// Supports locators with optional multiple states, enforcing correct state usage where applicable -// If no baseline is available, the element screenshot is retained for potential future use as a new baseline -// The baseline images were taken on a Pixel 6 (1080x2061) and an iPhone 16 Pro Max (1320x2868) -// -// Example usage: -// Locator with multiple states; -// await verifyElementScreenshot(device, new EmptyLandingPageScreenshot(device), 'new_account'); -// Locator with a single state: -// await verifyElementScreenshot(device, new SomeSimpleLocatorScreenshot(device)); +export async function pushAttachmentsToReport( + testInfo: TestInfo, + attachments: Attachment[] +): Promise { + for (const { name, body, contentType } of attachments) { + await testInfo.attach(name, { body, contentType }); + } +} + +/** + * Takes a screenshot of a UI element and verifies it against a saved baseline image. + * + * Requires Playwright's `testInfo` for attaching visual comparison artifacts to the test report. + * Supports locators with multiple states; enforces correct state usage via type constraints. + * If no baseline image exists, the element screenshot is saved and an error is thrown. + * On mismatch, a pixel-by-pixel comparison is performed and a visual diff is attached (when CI + ALLURE_ENABLED). + * Baseline screenshots are assumed to have been taken on:Pixel 6 (1080x2061) and iPhone 16 Pro Max (1320x2868) + * + * Example usage: + * // Locator with multiple states: + * await verifyElementScreenshot(device, new EmptyLandingPageScreenshot(device), testInfo, 'new_account'); + * + * // Locator with a single state: + * await verifyElementScreenshot(device, new SomeSimpleLocatorScreenshot(device), testInfo); + */ export async function verifyElementScreenshot< T extends LocatorsInterfaceScreenshot & { screenshotFileName: (...args: any[]) => string }, >( device: DeviceWrapper, element: T, + testInfo: TestInfo, ...args: Parameters // Enforces states when mandatory ): Promise { // Declaring a UUID in advance so that the diff and screenshot files are matched alphanumerically @@ -56,23 +77,57 @@ export async function verifyElementScreenshot< if (!equal) { const diffImagePath = path.join(diffsDir, `${uuid}_diffImage.png`); await diffImage.save(diffImagePath); - void allure.attachment( - 'Screenshot Diff', - fs.readFileSync(diffImagePath), - 'image/png' - ); - throw new Error(`The images do not match. The diff has been saved to ${diffImagePath}`); - } - // Cleanup of element screenshot file on success - try { - fs.unlinkSync(elementScreenshotPath); - console.log('Temporary screenshot deleted successfully'); - } catch (err) { - if (err instanceof Error) { - console.error(`Error deleting file: ${err.message}`); + + // For the CI, create a visual diff that renders in the Allure report + if (process.env.ALLURE_ENABLED === 'true' && process.env.CI === '1') { + // Load baseline and diff images + const baselineBase64 = fs.readFileSync(baselineScreenshotPath).toString('base64'); + const diffBase64 = fs.readFileSync(diffImagePath).toString('base64'); + + // Wrap them in the Allure visual diff format + const visualDiffPayload = { + actual: `data:image/png;base64,${elementScreenshotBase64}`, + expected: `data:image/png;base64,${baselineBase64}`, + diff: `data:image/png;base64,${diffBase64}`, + }; + + await pushAttachmentsToReport(testInfo, [ + { + name: 'Visual Comparison', + body: Buffer.from(JSON.stringify(visualDiffPayload), 'utf-8'), + contentType: 'application/vnd.allure.image.diff', + }, + { + name: 'Baseline Screenshot', + body: Buffer.from(baselineBase64, 'base64'), + contentType: 'image/png', + }, + { + name: 'Actual Screenshot', + body: Buffer.from(elementScreenshotBase64, 'base64'), + contentType: 'image/png', + }, + { + name: 'Diff Screenshot', + body: Buffer.from(diffBase64, 'base64'), + contentType: 'image/png', + }, + ]); + throw new Error(`The images do not match. The diff has been saved to ${diffImagePath}`); + } + + // Cleanup of element screenshot file on success + try { + fs.unlinkSync(elementScreenshotPath); + console.log('Temporary screenshot deleted successfully'); + } catch (err) { + if (err instanceof Error) { + console.error(`Error deleting file: ${err.message}`); + } } } } + export async function verifyPageScreenshot( platform: SupportedPlatformsType, device: DeviceWrapper, From d2d00ccf878930fce7517c8713a5a4f8bab1fe37 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 11:36:03 +1000 Subject: [PATCH 36/60] chore: linting --- run/types/DeviceWrapper.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 90473e8c..37bffd07 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1451,7 +1451,7 @@ export class DeviceWrapper { text: testVideo, maxWait: 5000, }); - // This codepath is purely for the CI + // This codepath is purely for the CI if (!videoElement) { // Try to reveal the video by selecting/filtering Videos in the native UI await this.clickOnElementAll({ @@ -1473,9 +1473,7 @@ export class DeviceWrapper { text: testVideo, }); } else { - throw new Error( - `Video "${testVideo}" not found after attempting to reveal it.` - ); + throw new Error(`Video "${testVideo}" not found after attempting to reveal it.`); } await this.waitForTextElementToBePresent({ ...new OutgoingMessageStatusSent(this).build(), @@ -1562,9 +1560,7 @@ export class DeviceWrapper { text: testFile, }); } else { - throw new Error( - `File "${testFile}" not found after attempting to reveal it.` - ); + throw new Error(`File "${testFile}" not found after attempting to reveal it.`); } await this.clickOnTextElementById('android:id/title', testFile); } From 8401d42132c862bd4de9e576d50d284f7d4dceda Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 11:40:08 +1000 Subject: [PATCH 37/60] revert: discard temp change to landing page screenshot --- run/screenshots/android/landingpage_new_account.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/screenshots/android/landingpage_new_account.png b/run/screenshots/android/landingpage_new_account.png index 686801d9..03808de9 100644 --- a/run/screenshots/android/landingpage_new_account.png +++ b/run/screenshots/android/landingpage_new_account.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2171f984e0476ac215b46aa47bda90392db1cea327cb1c7f375911454839eca -size 125515 +oid sha256:67df5e6fbc1b9c3be58275db41b3c6bc64911c0755e38196082062a585df457b +size 81721 From 2a34d939b08e7fffe6facdb890cafa2b2bf230bf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 11:41:05 +1000 Subject: [PATCH 38/60] linting again --- run/test/specs/landing_page_restore_account.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/run/test/specs/landing_page_restore_account.spec.ts b/run/test/specs/landing_page_restore_account.spec.ts index 8aa2c4eb..e3188a19 100644 --- a/run/test/specs/landing_page_restore_account.spec.ts +++ b/run/test/specs/landing_page_restore_account.spec.ts @@ -18,6 +18,11 @@ async function landingPageRestoreAccount(platform: SupportedPlatformsType, testI const { device1: alice1, device2: alice2 } = await openAppTwoDevices(platform); await linkedDevice(alice1, alice2, USERNAME.ALICE); // Verify that the Session logo is shown on the landing page - await verifyElementScreenshot(alice2, new EmptyLandingPageScreenshot(alice2), testInfo, 'restore_account'); + await verifyElementScreenshot( + alice2, + new EmptyLandingPageScreenshot(alice2), + testInfo, + 'restore_account' + ); await closeApp(alice1, alice2); } From 4e0e4b48081d9868e04a2d161dccf8e012240ca4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 13:41:48 +1000 Subject: [PATCH 39/60] feat: custom css injection for screenshot formatting --- run/test/specs/utils/allure/publishReport.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/run/test/specs/utils/allure/publishReport.ts b/run/test/specs/utils/allure/publishReport.ts index 8edcf4ed..5727afaf 100644 --- a/run/test/specs/utils/allure/publishReport.ts +++ b/run/test/specs/utils/allure/publishReport.ts @@ -36,6 +36,7 @@ function publishToGhPages(dir: string, dest: string, repo: string, message: stri ); }); } + async function publishReport() { // Define and create metadata.json for the front-end to fetch data from const platform = process.env.PLATFORM as SupportedPlatformsType; @@ -61,6 +62,24 @@ async function publishReport() { const publishedReportName = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; const newReportDir = path.join(platform, publishedReportName); + // --- Inject custom CSS before copying the report --- + const stylesPath = path.join(allureCurrentReportDir, 'styles.css'); + const customCss = ` + + /* Custom overrides */ + .attachment__media, + .screen-diff__image { + max-height: 90vh; + } + `; + + try { + await fs.appendFile(stylesPath, customCss); + console.log('Custom CSS injected into styles.css'); + } catch (err) { + console.error(`Failed to patch styles.css: ${(err as Error).message}`); + } + // Copy the current report to newReportDir for publishing // By doing so, the gh-pages branch hosts /android and /ios subpages with the respective reports try { From e9b2f7f53664c039e518b674cd43acb92453769f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 13:45:28 +1000 Subject: [PATCH 40/60] fix: comment --- run/test/specs/utils/allure/publishReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/utils/allure/publishReport.ts b/run/test/specs/utils/allure/publishReport.ts index 5727afaf..18d5a5ae 100644 --- a/run/test/specs/utils/allure/publishReport.ts +++ b/run/test/specs/utils/allure/publishReport.ts @@ -62,7 +62,7 @@ async function publishReport() { const publishedReportName = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; const newReportDir = path.join(platform, publishedReportName); - // --- Inject custom CSS before copying the report --- + // Inject custom CSS before copying the report const stylesPath = path.join(allureCurrentReportDir, 'styles.css'); const customCss = ` From 2866693ba74cdf65cd76aef0bdc72ac3172de712 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 14:42:19 +1000 Subject: [PATCH 41/60] refactor: use allureHelpers for all custom logic --- run/test/specs/utils/allure/allureHelpers.ts | 122 +++++++++++++++++++ run/test/specs/utils/allure/closeRun.ts | 40 +++--- run/test/specs/utils/allure/publishReport.ts | 76 +++++------- 3 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 run/test/specs/utils/allure/allureHelpers.ts diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts new file mode 100644 index 00000000..3e9592ef --- /dev/null +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -0,0 +1,122 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { execSync } from 'child_process'; +import { allureResultsDir, allureCurrentReportDir } from '../../../../constants/allure'; +import { SupportedPlatformsType } from '../open_app'; + +export interface ReportContext { + platform: SupportedPlatformsType; + build: string; + artifact: string; + risk: string; + runNumber: number; + runAttempt: number; + reportFolder: string; + reportUrl: string; + repoSlug: string; + githubRunUrl: string; +} + +/** + * Derives consistent context values from CI env + */ +export function getReportContextFromEnv(): ReportContext { + const platform = process.env.PLATFORM! as SupportedPlatformsType; + const build = process.env.BUILD_NUMBER!; + const artifact = process.env.APK_URL!; + const risk = process.env.RISK?.trim() || 'full'; + const runNumber = Number(process.env.GITHUB_RUN_NUMBER); + const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); + const repoSlug = process.env.GITHUB_REPOSITORY!; + const reportFolder = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; + const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${reportFolder}/`; + const githubRunUrl = `https://github.com/${repoSlug}/actions/runs/${process.env.GITHUB_RUN_ID}`; + + return { + platform, + build, + artifact, + risk, + runNumber, + runAttempt, + reportFolder, + reportUrl, + repoSlug, + githubRunUrl, + }; +} +// The Environment block shows up in the main report dashboard +export async function writeEnvironmentProperties(ctx: ReportContext) { + await fs.ensureDir(allureResultsDir); + const content = [ + `platform=${ctx.platform}`, + `build=${ctx.build}`, + `artifact=${ctx.artifact}`, + `appiumRepo=https://github.com/${ctx.repoSlug}/commit/${getGitCommitSha()} (${getGitBranch()})`, + ].join('\n'); + + await fs.writeFile(path.join(allureResultsDir, 'environment.properties'), content); + console.log('Created environment.properties'); +} +// The Executors block shows up in the main report and links back to the CI run +export async function writeExecutorJson(ctx: ReportContext) { + const buildOrder = ctx.runAttempt > 1 ? `${ctx.runNumber}.${ctx.runAttempt}` : `${ctx.runNumber}`; + const executor = { + name: 'GitHub Actions', + type: 'github', + url: ctx.githubRunUrl, + buildOrder: buildOrder, + buildName: `GitHub Actions Run ${buildOrder} (${ctx.platform} ${ctx.build})`, + buildUrl: ctx.githubRunUrl, + reportUrl: ctx.reportUrl, + }; + + await fs.writeFile( + path.join(allureResultsDir, 'executor.json'), + JSON.stringify(executor, null, 2) + ); + console.log('Created executor.json'); +} +// The metadata.json is a custom file for the front-end display +export async function writeMetadataJson(ctx: ReportContext) { + const metadata = { + platform: ctx.platform, + build: ctx.build, + risk: ctx.risk, + runNumber: ctx.runNumber, + runAttempt: ctx.runAttempt, + }; + + await fs.writeFile( + path.join(allureCurrentReportDir, 'metadata.json'), + JSON.stringify(metadata, null, 2) + ); + console.log('Created metadata.json'); +} + +// Custom css injection for neat diffing and media display +export async function patchStylesCss() { + const stylesPath = path.join(allureCurrentReportDir, 'styles.css'); + const customCss = ` + /* Custom overrides */ + .attachment__media, + .screen-diff__image { + max-height: 90vh; + } +`; + + try { + await fs.appendFile(stylesPath, customCss); + console.log('Patched styles.css with custom CSS'); + } catch (err) { + console.error(`Failed to patch styles.css: ${(err as Error).message}`); + } +} + +function getGitCommitSha(): string { + return execSync('git rev-parse HEAD').toString().trim(); +} + +function getGitBranch(): string { + return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); +} diff --git a/run/test/specs/utils/allure/closeRun.ts b/run/test/specs/utils/allure/closeRun.ts index fd4e1778..87206139 100644 --- a/run/test/specs/utils/allure/closeRun.ts +++ b/run/test/specs/utils/allure/closeRun.ts @@ -1,8 +1,7 @@ import fs from 'fs-extra'; -import path from 'path'; import { exec } from 'child_process'; import { allureCurrentReportDir, allureResultsDir } from '../../../../constants/allure'; -import { SupportedPlatformsType } from '../open_app'; +import { getReportContextFromEnv, writeEnvironmentProperties, writeExecutorJson } from './allureHelpers'; // Bail out early if not on CI if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { @@ -10,18 +9,18 @@ if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { process.exit(0); } -// Create environment.properties file with platform and build info -async function createEnvProperties( - platform: SupportedPlatformsType, - build: string, - artifact: string -) { - await fs.ensureDir(allureResultsDir); - const envPropertiesFile = path.join(allureResultsDir, 'environment.properties'); - const content = `platform=${platform}\nbuild=${build}\nartifact=${artifact}`; - await fs.writeFile(envPropertiesFile, content); - console.log(`Created environment.properties:\n${content}`); -} +// // Create environment.properties file with platform and build info +// async function createEnvProperties( +// platform: SupportedPlatformsType, +// build: string, +// artifact: string +// ) { +// await fs.ensureDir(allureResultsDir); +// const envPropertiesFile = path.join(allureResultsDir, 'environment.properties'); +// const content = `platform=${platform}\nbuild=${build}\nartifact=${artifact}`; +// await fs.writeFile(envPropertiesFile, content); +// console.log(`Created environment.properties:\n${content}`); +// } // Generate Allure report from the results directory async function generateAllureReport() { @@ -36,15 +35,14 @@ async function generateAllureReport() { }); } -// Close test run: handle histories, generate report, and clean up +// Close test run: manipulate custom files, generate report async function closeRun() { - // Read platform & build info from env - const platform = process.env.PLATFORM as SupportedPlatformsType; - const build = process.env.BUILD_NUMBER!; - const artifact = process.env.APK_URL!; - - await createEnvProperties(platform, build, artifact); + // Gather and write metadata files + const ctx = getReportContextFromEnv(); + await writeEnvironmentProperties(ctx); + await writeExecutorJson(ctx); + // Generate report await generateAllureReport(); // Clear allure-results directory for next run diff --git a/run/test/specs/utils/allure/publishReport.ts b/run/test/specs/utils/allure/publishReport.ts index 18d5a5ae..a9e31bbf 100644 --- a/run/test/specs/utils/allure/publishReport.ts +++ b/run/test/specs/utils/allure/publishReport.ts @@ -2,7 +2,7 @@ import fs from 'fs-extra'; import path from 'path'; import { allureCurrentReportDir } from '../../../../constants/allure'; import ghpages from 'gh-pages'; -import { SupportedPlatformsType } from '../open_app'; +import { getReportContextFromEnv, patchStylesCss, writeMetadataJson } from './allureHelpers'; // Bail out early if not on CI if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { @@ -38,52 +38,38 @@ function publishToGhPages(dir: string, dest: string, repo: string, message: stri } async function publishReport() { - // Define and create metadata.json for the front-end to fetch data from - const platform = process.env.PLATFORM as SupportedPlatformsType; - const build = process.env.BUILD_NUMBER!; - const runNumber = Number(process.env.GITHUB_RUN_NUMBER); - const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); - const risk = process.env.RISK?.trim() || 'full'; - - const metadata = { - platform, - build, - risk, - runNumber, - runAttempt, - }; - - fs.writeFileSync( - path.join(allureCurrentReportDir, 'metadata.json'), - JSON.stringify(metadata, null, 2) - ); + const ctx = getReportContextFromEnv(); + await writeMetadataJson(ctx); + // // Define and create metadata.json for the front-end to fetch data from + // const platform = process.env.PLATFORM as SupportedPlatformsType; + // const build = process.env.BUILD_NUMBER!; + // const runNumber = Number(process.env.GITHUB_RUN_NUMBER); + // const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); + // const risk = process.env.RISK?.trim() || 'full'; + + // const metadata = { + // platform, + // build, + // risk, + // runNumber, + // runAttempt, + // }; + + // fs.writeFileSync( + // path.join(allureCurrentReportDir, 'metadata.json'), + // JSON.stringify(metadata, null, 2) + // ); // Compose the published report directory name - const publishedReportName = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; - const newReportDir = path.join(platform, publishedReportName); + const publishedReportName = ctx.reportFolder; + const newReportDir = path.join(ctx.platform, publishedReportName); - // Inject custom CSS before copying the report - const stylesPath = path.join(allureCurrentReportDir, 'styles.css'); - const customCss = ` - - /* Custom overrides */ - .attachment__media, - .screen-diff__image { - max-height: 90vh; - } - `; - - try { - await fs.appendFile(stylesPath, customCss); - console.log('Custom CSS injected into styles.css'); - } catch (err) { - console.error(`Failed to patch styles.css: ${(err as Error).message}`); - } + await patchStylesCss(); // Copy the current report to newReportDir for publishing // By doing so, the gh-pages branch hosts /android and /ios subpages with the respective reports try { - await fs.ensureDir(platform); + await fs.ensureDir(ctx.platform); await fs.copy(allureCurrentReportDir, newReportDir, { overwrite: true }); console.log(`Report copied to ${newReportDir}`); } catch (err) { @@ -105,9 +91,9 @@ async function publishReport() { try { await publishToGhPages( newReportDir, - `${platform}/${publishedReportName}`, + `${ctx.platform}/${publishedReportName}`, repoWithToken, - `ci: publish Allure report for ${platform} ${build}` + `ci: publish Allure report for ${ctx.platform} ${ctx.build}` ); console.log(`Report deployed successfully as: ${publishedReportName}`); } catch (err) { @@ -115,15 +101,13 @@ async function publishReport() { process.exit(1); } - const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${publishedReportName}/`; - // Write the report URL to GitHub Actions output for downstream steps const githubOutputPath = process.env.GITHUB_OUTPUT; if (githubOutputPath) { - fs.appendFileSync(githubOutputPath, `report_url=${reportUrl}\n`); + fs.appendFileSync(githubOutputPath, `report_url=${ctx.reportUrl}\n`); console.log('Wrote report URL to GITHUB_OUTPUT'); } else { - console.log(`REPORT_URL=${reportUrl}`); + console.log(`REPORT_URL=${ctx.reportUrl}`); } } From bb70a6947d0972cfe43f252af8f2708bf421c3b4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 14:42:47 +1000 Subject: [PATCH 42/60] fix: message request tests on iOS --- .../specs/message_requests_accept.spec.ts | 2 +- .../specs/message_requests_decline.spec.ts | 19 ++++++++++++++----- .../specs/message_requests_delete.spec.ts | 19 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index 5b792359..cd24b4fc 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -41,7 +41,7 @@ async function acceptRequest(platform: SupportedPlatformsType) { ]); // Check conversation list for new contact (user A) await device2.navigateBack(); - await device2.navigateBack(false); + await device2.onAndroid().navigateBack(false); await Promise.all([ device2.waitForTextElementToBePresent({ strategy: 'accessibility id', diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index b10d8cc6..3c7d4a27 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -38,11 +38,20 @@ async function declineRequest(platform: SupportedPlatformsType) { await device2.clickOnElementAll(new DeclineMessageRequestButton(device2)); // Are you sure you want to delete message request only for ios await sleepFor(3000); - await device2.checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString(), - false - ); + // 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(), + false + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index 22d29ec3..17531fbb 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -27,11 +27,20 @@ async function deleteRequest(platform: SupportedPlatformsType) { await device2.onIOS().swipeLeftAny('Message request'); await device2.onAndroid().longPress('Message request'); await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); - await device2.checkModalStrings( - englishStrippedStr('delete').toString(), - englishStrippedStr('messageRequestsContactDelete').toString(), - false - ); + // 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(), + false + ); await device2.clickOnElementAll(new DeleteMesssageRequestConfirmation(device2)); // "messageRequestsNonePending": "No pending message requests", const messageRequestsNonePending = englishStrippedStr('messageRequestsNonePending').toString(); From 50b6b61b05d3900a02a204625de2c7930f027886 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 14:50:42 +1000 Subject: [PATCH 43/60] revert changes to Android CI scripting --- .github/workflows/android-regression.yml | 5 ++--- scripts/ci.sh | 27 ------------------------ 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 590672b5..b2bffe35 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -141,11 +141,10 @@ jobs: adb kill-server; adb start-server; - - name: Start emulators (clean boot) + - name: Start emulators from snapshot run: | source ./scripts/ci.sh - start_without_snapshots - wait_for_emulators + start_with_snapshots - name: List tests part of this run uses: ./github/actions/list-tests diff --git a/scripts/ci.sh b/scripts/ci.sh index a966215d..dac3a18c 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -97,30 +97,3 @@ function start_with_snapshots() { sleep 5 done } - -function start_without_snapshots() { - for i in {1..4} - do - DISPLAY=:0 emulator @emulator$i \ - -gpu host \ - -no-snapshot \ - -wipe-data \ - -accel on & - sleep 20 - done -} - -function wait_for_emulators() { - for port in 5554 5556 5558 5560 - do - for i in {1..60}; do - if adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then - echo "emulator-$port booted" - break - else - echo "Waiting for emulator-$port to boot..." - sleep 5 - fi - done - done -} \ No newline at end of file From db8885f37349e94485c78af37c06392256c61e51 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:27:24 +1000 Subject: [PATCH 44/60] fix: include RISK input in report generation and clean up metadata handling --- .../generate-publish-test-report/action.yml | 1 + run/test/specs/utils/allure/allureHelpers.ts | 3 ++- run/test/specs/utils/allure/closeRun.ts | 6 +++++- run/test/specs/utils/allure/publishReport.ts | 19 ------------------- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml index cc49bfb6..ecab702e 100644 --- a/github/actions/generate-publish-test-report/action.yml +++ b/github/actions/generate-publish-test-report/action.yml @@ -28,6 +28,7 @@ runs: BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }} GH_TOKEN: ${{ inputs.GH_TOKEN }} APK_URL: ${{inputs.APK_URL}} + RISK: ${{inputs.RISK}} - name: Publish report to GitHub Pages if: ${{ success() }} diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 3e9592ef..3d173a94 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -52,7 +52,8 @@ export async function writeEnvironmentProperties(ctx: ReportContext) { `platform=${ctx.platform}`, `build=${ctx.build}`, `artifact=${ctx.artifact}`, - `appiumRepo=https://github.com/${ctx.repoSlug}/commit/${getGitCommitSha()} (${getGitBranch()})`, + `appium=https://github.com/${ctx.repoSlug}/commit/${getGitCommitSha()}`, + `branch=${getGitBranch()}`, ].join('\n'); await fs.writeFile(path.join(allureResultsDir, 'environment.properties'), content); diff --git a/run/test/specs/utils/allure/closeRun.ts b/run/test/specs/utils/allure/closeRun.ts index 87206139..74ebc20f 100644 --- a/run/test/specs/utils/allure/closeRun.ts +++ b/run/test/specs/utils/allure/closeRun.ts @@ -1,7 +1,11 @@ import fs from 'fs-extra'; import { exec } from 'child_process'; import { allureCurrentReportDir, allureResultsDir } from '../../../../constants/allure'; -import { getReportContextFromEnv, writeEnvironmentProperties, writeExecutorJson } from './allureHelpers'; +import { + getReportContextFromEnv, + writeEnvironmentProperties, + writeExecutorJson, +} from './allureHelpers'; // Bail out early if not on CI if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { diff --git a/run/test/specs/utils/allure/publishReport.ts b/run/test/specs/utils/allure/publishReport.ts index a9e31bbf..5b82f367 100644 --- a/run/test/specs/utils/allure/publishReport.ts +++ b/run/test/specs/utils/allure/publishReport.ts @@ -40,25 +40,6 @@ function publishToGhPages(dir: string, dest: string, repo: string, message: stri async function publishReport() { const ctx = getReportContextFromEnv(); await writeMetadataJson(ctx); - // // Define and create metadata.json for the front-end to fetch data from - // const platform = process.env.PLATFORM as SupportedPlatformsType; - // const build = process.env.BUILD_NUMBER!; - // const runNumber = Number(process.env.GITHUB_RUN_NUMBER); - // const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); - // const risk = process.env.RISK?.trim() || 'full'; - - // const metadata = { - // platform, - // build, - // risk, - // runNumber, - // runAttempt, - // }; - - // fs.writeFileSync( - // path.join(allureCurrentReportDir, 'metadata.json'), - // JSON.stringify(metadata, null, 2) - // ); // Compose the published report directory name const publishedReportName = ctx.reportFolder; From cb9e1c472d2c297d72bcd8bdcbdf9d8c5168b209 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:27:47 +1000 Subject: [PATCH 45/60] fix: wait before matchAndTapImage --- run/types/DeviceWrapper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 37bffd07..2708ef83 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1354,7 +1354,7 @@ export class DeviceWrapper { } await sleepFor(1000); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - // await verifyElementScreenshot(this, new DummyScreenshot(this)); + await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, testImage @@ -1414,8 +1414,10 @@ export class DeviceWrapper { selector: 'Allow Full Access', maxWait: 500, }); + await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise // For some reason video gets added to the top of the Recents folder so it's best to scroll up await this.scrollUp(); + await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise // A video can't be matched by its thumbnail so we use a video thumbnail file await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, From f22e995595a377f3779e27beb94a3186b5b5a0d3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:27:58 +1000 Subject: [PATCH 46/60] fix: Android CI tweaks --- .github/workflows/android-regression.yml | 1 + scripts/ci.sh | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index b2bffe35..2dd156cd 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -142,6 +142,7 @@ jobs: adb start-server; - name: Start emulators from snapshot + shell: bash run: | source ./scripts/ci.sh start_with_snapshots diff --git a/scripts/ci.sh b/scripts/ci.sh index dac3a18c..e32c09af 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -96,4 +96,25 @@ function start_with_snapshots() { DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & sleep 5 done + + echo "Waiting for emulators to finish booting..." + + for port in 5554 5556 5558 5560; do + echo "Waiting on emulator-$port..." + for i in {1..60}; do + booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$booted" == "1" ]; then + echo "emulator-$port booted." + break + fi + sleep 2 + done + + booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$booted" != "1" ]; then + echo "ERROR: emulator-$port failed to boot within timeout." + adb devices + exit 1 + fi + done } From c334c19e736abc711db0edc427d0badd4304380a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:36:28 +1000 Subject: [PATCH 47/60] fix: wait for emulators to become responsive --- scripts/ci.sh | 82 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index e32c09af..d7a84470 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -87,34 +87,60 @@ function killall_emulators() { function start_with_snapshots() { - for i in {1..4} - do - EMU_CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/emulator-user.ini" - # set the position fo each emulator to be next to the previous one - sed -i "s/^window.x.*/window.x=$(( 100 + (i-1) * 400))/" "$EMU_CONFIG_FILE" - - DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & - sleep 5 + for i in {1..4}; do + EMU_CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/emulator-user.ini" + # Set window position (optional in headless) + sed -i "s/^window.x.*/window.x=$(( 100 + (i-1) * 400))/" "$EMU_CONFIG_FILE" + + DISPLAY=:0 emulator @emulator$i \ + -gpu host \ + -accel on \ + -no-snapshot-save \ + -snapshot plop.snapshot \ + -force-snapshot-load \ + -no-window & + + sleep 5 + done + + echo "Waiting for emulators to become responsive..." + + for port in 5554 5556 5558 5560; do + echo "Waiting for emulator-$port to become ADB responsive..." + + # Wait for ADB to report 'device' instead of 'offline' + for i in {1..30}; do + state=$(adb -s emulator-$port get-state 2>/dev/null | tr -d '\r') + if [ "$state" == "device" ]; then + echo "emulator-$port is now ADB-ready." + break + fi + sleep 2 done - - echo "Waiting for emulators to finish booting..." - - for port in 5554 5556 5558 5560; do - echo "Waiting on emulator-$port..." - for i in {1..60}; do - booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [ "$booted" == "1" ]; then - echo "emulator-$port booted." - break - fi - sleep 2 - done - - booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [ "$booted" != "1" ]; then - echo "ERROR: emulator-$port failed to boot within timeout." - adb devices - exit 1 - fi + + # Optional: if still not 'device', exit early + state=$(adb -s emulator-$port get-state 2>/dev/null | tr -d '\r') + if [ "$state" != "device" ]; then + echo "ERROR: emulator-$port stuck in '$state' state after timeout." + adb devices + exit 1 + fi + + echo "Waiting for emulator-$port to report boot completed..." + for i in {1..60}; do + booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$booted" == "1" ]; then + echo "emulator-$port booted." + break + fi + sleep 2 done + + booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$booted" != "1" ]; then + echo "ERROR: emulator-$port failed to boot in time." + adb devices + exit 1 + fi + done } From 030ed1bae2732a718e14cc87303d262d07b89ebb Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:41:53 +1000 Subject: [PATCH 48/60] fix: wait for simulators to appear in adb --- .github/workflows/android-regression.yml | 1 + scripts/ci.sh | 39 +++++++++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 2dd156cd..130231e2 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -146,6 +146,7 @@ jobs: run: | source ./scripts/ci.sh start_with_snapshots + wait_for_emulators - name: List tests part of this run uses: ./github/actions/list-tests diff --git a/scripts/ci.sh b/scripts/ci.sh index d7a84470..92ac11a4 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -103,42 +103,53 @@ function start_with_snapshots() { sleep 5 done - echo "Waiting for emulators to become responsive..." +function wait_for_emulators() { + echo "Waiting for emulators to boot and respond to adb..." for port in 5554 5556 5558 5560; do - echo "Waiting for emulator-$port to become ADB responsive..." + serial="emulator-$port" - # Wait for ADB to report 'device' instead of 'offline' - for i in {1..30}; do - state=$(adb -s emulator-$port get-state 2>/dev/null | tr -d '\r') + echo "Waiting for $serial to appear in adb..." + for i in {1..60}; do + if adb devices | grep -q "$serial"; then + break + fi + sleep 2 + done + + if ! adb devices | grep -q "$serial"; then + echo "ERROR: $serial did not appear in adb within timeout." + adb devices + exit 1 + fi + + echo "$serial appeared. Waiting for adb to report 'device' state..." + for i in {1..60}; do + state=$(adb -s "$serial" get-state 2>/dev/null | tr -d '\r') if [ "$state" == "device" ]; then - echo "emulator-$port is now ADB-ready." break fi sleep 2 done - # Optional: if still not 'device', exit early - state=$(adb -s emulator-$port get-state 2>/dev/null | tr -d '\r') if [ "$state" != "device" ]; then - echo "ERROR: emulator-$port stuck in '$state' state after timeout." + echo "ERROR: $serial is in '$state' state after timeout." adb devices exit 1 fi - echo "Waiting for emulator-$port to report boot completed..." + echo "$serial is now adb-accessible. Waiting for system boot..." for i in {1..60}; do - booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + booted=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') if [ "$booted" == "1" ]; then - echo "emulator-$port booted." + echo "$serial boot completed." break fi sleep 2 done - booted=$(adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') if [ "$booted" != "1" ]; then - echo "ERROR: emulator-$port failed to boot in time." + echo "ERROR: $serial failed to complete boot." adb devices exit 1 fi From b54ccc4b30543686c22edd427f5a27bf92ddab7d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:44:55 +1000 Subject: [PATCH 49/60] fix unexpected end of file --- scripts/ci.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci.sh b/scripts/ci.sh index 92ac11a4..860d22ea 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -102,6 +102,7 @@ function start_with_snapshots() { sleep 5 done +} function wait_for_emulators() { echo "Waiting for emulators to boot and respond to adb..." From d5e0be75e0c3029f3e03be2d092c1122fe0b9db1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Jun 2025 16:49:18 +1000 Subject: [PATCH 50/60] wip: wait_for_emulators --- scripts/ci.sh | 62 ++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 50 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 860d22ea..becf6900 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -105,54 +105,16 @@ function start_with_snapshots() { } function wait_for_emulators() { - echo "Waiting for emulators to boot and respond to adb..." - - for port in 5554 5556 5558 5560; do - serial="emulator-$port" - - echo "Waiting for $serial to appear in adb..." - for i in {1..60}; do - if adb devices | grep -q "$serial"; then - break - fi - sleep 2 - done - - if ! adb devices | grep -q "$serial"; then - echo "ERROR: $serial did not appear in adb within timeout." - adb devices - exit 1 - fi - - echo "$serial appeared. Waiting for adb to report 'device' state..." - for i in {1..60}; do - state=$(adb -s "$serial" get-state 2>/dev/null | tr -d '\r') - if [ "$state" == "device" ]; then - break - fi - sleep 2 - done - - if [ "$state" != "device" ]; then - echo "ERROR: $serial is in '$state' state after timeout." - adb devices - exit 1 - fi - - echo "$serial is now adb-accessible. Waiting for system boot..." - for i in {1..60}; do - booted=$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [ "$booted" == "1" ]; then - echo "$serial boot completed." - break - fi - sleep 2 + for port in 5554 5556 5558 5560 + do + for i in {1..60}; do + if adb -s emulator-$port shell getprop sys.boot_completed 2>/dev/null | grep -q "1"; then + echo "emulator-$port booted" + break + else + echo "Waiting for emulator-$port to boot..." + sleep 5 + fi + done done - - if [ "$booted" != "1" ]; then - echo "ERROR: $serial failed to complete boot." - adb devices - exit 1 - fi - done -} +} \ No newline at end of file From e8f95ca54f79f66f25b3f54c83622f91fb068a05 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Jun 2025 09:42:45 +1000 Subject: [PATCH 51/60] fix: update report context to remove repoSlug and hardcode GitHub URL --- run/test/specs/utils/allure/allureHelpers.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 3d173a94..aef48bfe 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -13,7 +13,6 @@ export interface ReportContext { runAttempt: number; reportFolder: string; reportUrl: string; - repoSlug: string; githubRunUrl: string; } @@ -27,10 +26,9 @@ export function getReportContextFromEnv(): ReportContext { const risk = process.env.RISK?.trim() || 'full'; const runNumber = Number(process.env.GITHUB_RUN_NUMBER); const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); - const repoSlug = process.env.GITHUB_REPOSITORY!; const reportFolder = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${reportFolder}/`; - const githubRunUrl = `https://github.com/${repoSlug}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const githubRunUrl = `https://github.com/session-foundation/session-appium/actions/runs/${process.env.GITHUB_RUN_ID}`; return { platform, @@ -41,33 +39,33 @@ export function getReportContextFromEnv(): ReportContext { runAttempt, reportFolder, reportUrl, - repoSlug, githubRunUrl, }; } -// The Environment block shows up in the main report dashboard +// The Environment block shows up in the report dashboard export async function writeEnvironmentProperties(ctx: ReportContext) { await fs.ensureDir(allureResultsDir); const content = [ `platform=${ctx.platform}`, `build=${ctx.build}`, `artifact=${ctx.artifact}`, - `appium=https://github.com/${ctx.repoSlug}/commit/${getGitCommitSha()}`, + `appium=https://github.com/session-foundation/session-appium/commit/${getGitCommitSha()}`, `branch=${getGitBranch()}`, ].join('\n'); await fs.writeFile(path.join(allureResultsDir, 'environment.properties'), content); console.log('Created environment.properties'); } -// The Executors block shows up in the main report and links back to the CI run +// The Executors block shows up in the report dashboard and links back to the CI run +// It also allows us to access history through trend graphs and test results export async function writeExecutorJson(ctx: ReportContext) { - const buildOrder = ctx.runAttempt > 1 ? `${ctx.runNumber}.${ctx.runAttempt}` : `${ctx.runNumber}`; + const buildOrder = ctx.runAttempt > 1 ? `${ctx.runNumber}.${ctx.runAttempt}` : `${ctx.runNumber}`; const executor = { name: 'GitHub Actions', type: 'github', url: ctx.githubRunUrl, buildOrder: buildOrder, - buildName: `GitHub Actions Run ${buildOrder} (${ctx.platform} ${ctx.build})`, + buildName: `GitHub Actions Run ${ctx.githubRunUrl}`, buildUrl: ctx.githubRunUrl, reportUrl: ctx.reportUrl, }; From d6e07dbba1da2d14e34fcd9636ce7d92be11123a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Jun 2025 09:43:04 +1000 Subject: [PATCH 52/60] fix: ios change --- run/test/specs/group_tests_add_contact.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 760e0b3a..cd79c981 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -61,10 +61,10 @@ async function addContactToGroup(platform: SupportedPlatformsType) { ) ) ); - // Leave conversation - await unknown1.navigateBack(); - // Leave Message Requests screen + // Leave conversation 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.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); From baac0c465394e892f940dd91a29abc5452b1376d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Jun 2025 10:41:45 +1000 Subject: [PATCH 53/60] fix: update ReportContext to derive runID and adjust GitHub run URL --- run/test/specs/utils/allure/allureHelpers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index aef48bfe..05450669 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -11,6 +11,7 @@ export interface ReportContext { risk: string; runNumber: number; runAttempt: number; + runID: number, reportFolder: string; reportUrl: string; githubRunUrl: string; @@ -26,9 +27,10 @@ export function getReportContextFromEnv(): ReportContext { const risk = process.env.RISK?.trim() || 'full'; const runNumber = Number(process.env.GITHUB_RUN_NUMBER); const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); + const runID = Number(process.env.GITHUB_RUN_ID) const reportFolder = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${reportFolder}/`; - const githubRunUrl = `https://github.com/session-foundation/session-appium/actions/runs/${process.env.GITHUB_RUN_ID}`; + const githubRunUrl = `https://github.com/session-foundation/session-appium/actions/runs/${runID}`; return { platform, @@ -37,6 +39,7 @@ export function getReportContextFromEnv(): ReportContext { risk, runNumber, runAttempt, + runID, reportFolder, reportUrl, githubRunUrl, @@ -65,7 +68,7 @@ export async function writeExecutorJson(ctx: ReportContext) { type: 'github', url: ctx.githubRunUrl, buildOrder: buildOrder, - buildName: `GitHub Actions Run ${ctx.githubRunUrl}`, + buildName: `GitHub Actions Run #${ctx.runID}`, buildUrl: ctx.githubRunUrl, reportUrl: ctx.reportUrl, }; From deb1922b01e001327ded96322e65f333dc9e2e2e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 27 Jun 2025 14:44:28 +1000 Subject: [PATCH 54/60] fix: start emulators with head & reduce ram to 4GB --- scripts/ci.sh | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index becf6900..9b170c2d 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -55,8 +55,8 @@ function create_emulators() { # Path to the AVD's config.ini file CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/config.ini" - # Set the RAM size to 6GB (6144MB) - sed -i 's/^hw\.ramSize=.*/hw.ramSize=6144/' "$CONFIG_FILE" + # Set the RAM size to GB (4192MB) + sed -i 's/^hw\.ramSize=.*/hw.ramSize=4192/' "$CONFIG_FILE" done @@ -74,7 +74,7 @@ function start_for_snapshots() { # let the emulators start and be ready (check cpu usage) before calling this. # We want to take a snapshot woth emulators state as "done" as we can function force_save_snapshots() { - values=("5554" "5556" "5558" "5560" "5562" "5564" "5566" "5568") + values=("5554" "5556" "5558" "5560") for val in "${values[@]}" do adb -s emulator-$val emu avd snapshot save plop.snapshot @@ -92,17 +92,12 @@ function start_with_snapshots() { # Set window position (optional in headless) sed -i "s/^window.x.*/window.x=$(( 100 + (i-1) * 400))/" "$EMU_CONFIG_FILE" - DISPLAY=:0 emulator @emulator$i \ - -gpu host \ - -accel on \ - -no-snapshot-save \ - -snapshot plop.snapshot \ - -force-snapshot-load \ - -no-window & + # DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load -no-window & + DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & sleep 5 done -} +} function wait_for_emulators() { for port in 5554 5556 5558 5560 @@ -117,4 +112,7 @@ function wait_for_emulators() { fi done done -} \ No newline at end of file +} + + +set +x \ No newline at end of file From 966b5b6ae8b4b6ca70244a936b6979c8da5ce595 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Jun 2025 15:07:22 +1000 Subject: [PATCH 55/60] fix: document sending only taps media file once --- run/types/DeviceWrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2708ef83..6ecc2e20 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1564,7 +1564,6 @@ export class DeviceWrapper { } else { throw new Error(`File "${testFile}" not found after attempting to reveal it.`); } - await this.clickOnTextElementById('android:id/title', testFile); } // Checking Sent status on both platforms await this.waitForTextElementToBePresent({ From ce7d586621127c43f2d69b038a17cee584801ed8 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Jun 2025 15:07:52 +1000 Subject: [PATCH 56/60] fix: remove unused code from closeRun --- run/test/specs/utils/allure/closeRun.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/run/test/specs/utils/allure/closeRun.ts b/run/test/specs/utils/allure/closeRun.ts index 74ebc20f..dba572aa 100644 --- a/run/test/specs/utils/allure/closeRun.ts +++ b/run/test/specs/utils/allure/closeRun.ts @@ -13,19 +13,6 @@ if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { process.exit(0); } -// // Create environment.properties file with platform and build info -// async function createEnvProperties( -// platform: SupportedPlatformsType, -// build: string, -// artifact: string -// ) { -// await fs.ensureDir(allureResultsDir); -// const envPropertiesFile = path.join(allureResultsDir, 'environment.properties'); -// const content = `platform=${platform}\nbuild=${build}\nartifact=${artifact}`; -// await fs.writeFile(envPropertiesFile, content); -// console.log(`Created environment.properties:\n${content}`); -// } - // Generate Allure report from the results directory async function generateAllureReport() { return new Promise((resolve, reject) => { From 3b4e83dbc98a7817e5b51fbd7d6ee4302599eb7a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Jun 2025 15:50:37 +1000 Subject: [PATCH 57/60] fix: add missing env variables for closeRun composite action --- github/actions/generate-publish-test-report/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml index ecab702e..7e29fc26 100644 --- a/github/actions/generate-publish-test-report/action.yml +++ b/github/actions/generate-publish-test-report/action.yml @@ -29,6 +29,8 @@ runs: GH_TOKEN: ${{ inputs.GH_TOKEN }} APK_URL: ${{inputs.APK_URL}} RISK: ${{inputs.RISK}} + GITHUB_RUN_NUMBER: ${{ inputs.GITHUB_RUN_NUMBER}} + GITHUB_RUN_ATTEMPT: ${{ inputs.GITHUB_RUN_ATTEMPT}} - name: Publish report to GitHub Pages if: ${{ success() }} From d77cd356aa839244add5b22ca3de8cbd808a0736 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Jun 2025 15:51:11 +1000 Subject: [PATCH 58/60] chore: linting --- run/test/specs/group_tests_add_contact.spec.ts | 2 +- run/test/specs/utils/allure/allureHelpers.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index cd79c981..6c4cd9ca 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -61,7 +61,7 @@ async function addContactToGroup(platform: SupportedPlatformsType) { ) ) ); - // Leave conversation + // Leave conversation await unknown1.navigateBack(); // Leave Message Requests screen (Android) await unknown1.onAndroid().navigateBack(); diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 05450669..0c148d4f 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -11,7 +11,7 @@ export interface ReportContext { risk: string; runNumber: number; runAttempt: number; - runID: number, + runID: number; reportFolder: string; reportUrl: string; githubRunUrl: string; @@ -27,7 +27,7 @@ export function getReportContextFromEnv(): ReportContext { const risk = process.env.RISK?.trim() || 'full'; const runNumber = Number(process.env.GITHUB_RUN_NUMBER); const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); - const runID = Number(process.env.GITHUB_RUN_ID) + const runID = Number(process.env.GITHUB_RUN_ID); const reportFolder = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${reportFolder}/`; const githubRunUrl = `https://github.com/session-foundation/session-appium/actions/runs/${runID}`; @@ -62,7 +62,7 @@ export async function writeEnvironmentProperties(ctx: ReportContext) { // The Executors block shows up in the report dashboard and links back to the CI run // It also allows us to access history through trend graphs and test results export async function writeExecutorJson(ctx: ReportContext) { - const buildOrder = ctx.runAttempt > 1 ? `${ctx.runNumber}.${ctx.runAttempt}` : `${ctx.runNumber}`; + const buildOrder = ctx.runAttempt > 1 ? `${ctx.runNumber}.${ctx.runAttempt}` : `${ctx.runNumber}`; const executor = { name: 'GitHub Actions', type: 'github', From 4864beb9a11031d60ae8973123d583a307ce9ea9 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Jun 2025 16:16:35 +1000 Subject: [PATCH 59/60] finishing touches --- run/test/specs/linked_group_leave.spec.ts | 1 - scripts/ci.sh | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/run/test/specs/linked_group_leave.spec.ts b/run/test/specs/linked_group_leave.spec.ts index 5f96a35a..c8556a7f 100644 --- a/run/test/specs/linked_group_leave.spec.ts +++ b/run/test/specs/linked_group_leave.spec.ts @@ -38,7 +38,6 @@ async function leaveGroupLinkedDevice(platform: SupportedPlatformsType) { // Modal with Leave/Cancel await device3.clickOnElementAll(new LeaveGroupConfirm(device3)); // Check for control message - // Check for control message await sleepFor(5000); await device4.onIOS().hasTextElementBeenDeleted('Conversation list item', testGroupName); // Create control message for user leaving group diff --git a/scripts/ci.sh b/scripts/ci.sh index 9b170c2d..647c403e 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -55,7 +55,7 @@ function create_emulators() { # Path to the AVD's config.ini file CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/config.ini" - # Set the RAM size to GB (4192MB) + # Set the RAM size to 4GB (4192MB) sed -i 's/^hw\.ramSize=.*/hw.ramSize=4192/' "$CONFIG_FILE" done @@ -89,10 +89,9 @@ function killall_emulators() { function start_with_snapshots() { for i in {1..4}; do EMU_CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/emulator-user.ini" - # Set window position (optional in headless) + # Set window position sed -i "s/^window.x.*/window.x=$(( 100 + (i-1) * 400))/" "$EMU_CONFIG_FILE" - # DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load -no-window & DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & sleep 5 From 615630c93366bfbdc8c9c0327cba97a6b53196e4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Jun 2025 10:54:23 +1000 Subject: [PATCH 60/60] fix: address PR comments --- run/constants/allure.ts | 1 + .../specs/linked_device_create_group.spec.ts | 4 +-- run/test/specs/utils/allure/allureHelpers.ts | 33 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/run/constants/allure.ts b/run/constants/allure.ts index f3f514c5..b05b6fda 100644 --- a/run/constants/allure.ts +++ b/run/constants/allure.ts @@ -3,3 +3,4 @@ import path from 'path'; export const allureResultsDir = path.join('allure', 'allure-results'); export const allureCurrentReportDir = path.join('allure', 'allure-report'); export const allureReportsDir = path.join('allure', 'reports'); +export const GH_PAGES_BASE_URL = 'https://session-foundation.github.io/session-appium/'; diff --git a/run/test/specs/linked_device_create_group.spec.ts b/run/test/specs/linked_device_create_group.spec.ts index 7e1d5b23..71cca47f 100644 --- a/run/test/specs/linked_device_create_group.spec.ts +++ b/run/test/specs/linked_device_create_group.spec.ts @@ -48,8 +48,8 @@ async function linkedGroupiOS(platform: SupportedPlatformsType) { await device1.clickOnElementAll(new UpdateGroupInformation(device1, testGroupName)); // Check new dialog await device1.checkModalStrings( - englishStrippedStr(`updateGroupInformation`).toString(), - englishStrippedStr(`updateGroupInformationDescription`).toString() + englishStrippedStr('updateGroupInformation').toString(), + englishStrippedStr('updateGroupInformationDescription').toString() ); // Delete old name first await device1.deleteText(new EditGroupNameInput(device1)); diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index 0c148d4f..84e7a9b7 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -1,7 +1,11 @@ import fs from 'fs-extra'; import path from 'path'; import { execSync } from 'child_process'; -import { allureResultsDir, allureCurrentReportDir } from '../../../../constants/allure'; +import { + allureResultsDir, + allureCurrentReportDir, + GH_PAGES_BASE_URL, +} from '../../../../constants/allure'; import { SupportedPlatformsType } from '../open_app'; export interface ReportContext { @@ -21,17 +25,36 @@ export interface ReportContext { * Derives consistent context values from CI env */ export function getReportContextFromEnv(): ReportContext { - const platform = process.env.PLATFORM! as SupportedPlatformsType; - const build = process.env.BUILD_NUMBER!; - const artifact = process.env.APK_URL!; + const platform = process.env.PLATFORM as SupportedPlatformsType | undefined; + const build = process.env.BUILD_NUMBER; + const artifact = process.env.APK_URL; const risk = process.env.RISK?.trim() || 'full'; const runNumber = Number(process.env.GITHUB_RUN_NUMBER); const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); const runID = Number(process.env.GITHUB_RUN_ID); const reportFolder = `run-${runNumber}.${runAttempt}-${platform}-${build}-${risk}`; - const reportUrl = `https://session-foundation.github.io/session-appium/${platform}/${reportFolder}/`; + const reportUrl = `${GH_PAGES_BASE_URL}/${platform}/${reportFolder}/`; const githubRunUrl = `https://github.com/session-foundation/session-appium/actions/runs/${runID}`; + if (!platform) { + throw new Error('PLATFORM env variable is required'); + } + if (!build) { + throw new Error('BUILD_NUMBER env variable is required'); + } + if (!artifact) { + throw new Error('APK_URL env variable is required'); + } + if (!runNumber) { + throw new Error('GITHUB_RUN_NUMBER env variable is required'); + } + if (!runAttempt) { + throw new Error('GITHUB_RUN_ATTEMPT env variable is required'); + } + if (!runID) { + throw new Error('GITHUB_RUN_ID env variable is required'); + } + return { platform, build,