diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml
index b697d469..130231e2 100644
--- a/.github/workflows/android-regression.yml
+++ b/.github/workflows/android-regression.yml
@@ -141,11 +141,12 @@ jobs:
adb kill-server;
adb start-server;
- - name: Start emulators
+ - name: Start emulators from snapshot
shell: bash
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/.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/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml
index cc49bfb6..7e29fc26 100644
--- a/github/actions/generate-publish-test-report/action.yml
+++ b/github/actions/generate-publish-test-report/action.yml
@@ -28,6 +28,9 @@ runs:
BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }}
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() }}
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..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..3c8c6ab0b8c08ab6519268465e3c5bfd06068f86 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..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, 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/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/localizer/constants.ts b/run/localizer/constants.ts
index 7ff7fec1..6fd2b45b 100644
--- a/run/localizer/constants.ts
+++ b/run/localizer/constants.ts
@@ -8,7 +8,6 @@ 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',
}
diff --git a/run/localizer/locales.ts b/run/localizer/locales.ts
index 2be47598..3ec1b4ae 100644
--- a/run/localizer/locales.ts
+++ b/run/localizer/locales.ts
@@ -1843,7 +1843,7 @@ export const simpleDictionary = {
args: undefined,
},
groupNameEnterShorter: {
- en: 'Please enter a shorter group name.',
+ en: 'Please enter a shorter group name',
args: undefined,
},
groupNameNew: {
@@ -2222,6 +2222,10 @@ export const simpleDictionary = {
en: 'Message',
args: undefined,
},
+ messageBubbleReadMore: {
+ en: 'Read more',
+ args: undefined,
+ },
messageEmpty: {
en: 'This message is empty.',
args: undefined,
@@ -2386,12 +2390,24 @@ export const simpleDictionary = {
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',
+ en: 'Message Too Long',
args: undefined,
},
next: {
@@ -3115,11 +3131,7 @@ export const simpleDictionary = {
args: undefined,
},
remainingCharactersOverTooltip: {
- en: 'Message is too long',
- args: undefined,
- },
- remainingCharactersTooltip: {
- en: '{count} characters remaining',
+ en: 'Reduce message length by {count}',
args: { count: 'number' },
},
remove: {
@@ -3570,6 +3582,10 @@ export const simpleDictionary = {
en: 'You',
args: undefined,
},
+ sessionNetworkDataPrice: {
+ en: 'Price data powered by CoinGecko
Accurate at {date_time}',
+ args: { date_time: 'string' },
+ },
} as const;
export const pluralsDictionary = {
@@ -3720,6 +3736,14 @@ export const pluralsDictionary = {
},
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: { limit: 'string', count: 'number' },
+ },
promotionFailed: {
en: {
one: 'Promotion Failed',
@@ -3734,6 +3758,13 @@ export const pluralsDictionary = {
},
args: { count: 'number' },
},
+ remainingCharactersTooltip: {
+ en: {
+ one: '{count} character remaining',
+ other: '{count} characters remaining',
+ },
+ args: { count: 'number' },
+ },
searchMatches: {
en: {
one: '{found_count} of {count} match',
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
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/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
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/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/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 230ad2f4..feb50fe7 100644
--- a/run/test/specs/community_tests_join.spec.ts
+++ b/run/test/specs/community_tests_join.spec.ts
@@ -1,6 +1,8 @@
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';
@@ -18,13 +20,10 @@ 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({
- 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 9ce095ee..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,7 +1,7 @@
import { bothPlatformsIt } from '../../types/sessionIt';
import { DisappearActions, DISAPPEARING_TIMES, USERNAME } from '../../types/testing';
-import { MessageInput } from './locators/conversation';
-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';
@@ -21,12 +21,15 @@ 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.inputText('Creating note to self', new MessageInput(device));
+ await device.clickOnElementAll(new NextButton(device));
+ await device.inputText('Creating note to self', {
+ strategy: 'accessibility id',
+ selector: 'Message input box',
+ });
await device.clickOnByAccessibilityID('Send message button');
// Enable disappearing messages
await setDisappearingMessage(platform, device, [
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_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/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts
index 7a43b5d6..bb32c592 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,11 @@ 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.clickOnElementAll(new GroupMember(alice1).build(bob.userName));
+ await alice1.clickOnElementAll({
+ strategy: 'id',
+ selector: 'invite-contacts-button',
});
- await alice1.clickOnByAccessibilityID('Done');
// 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..0cef35ef 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/donate.spec.ts b/run/test/specs/donate.spec.ts
index b414eafd..f4aed0d2 100644
--- a/run/test/specs/donate.spec.ts
+++ b/run/test/specs/donate.spec.ts
@@ -3,10 +3,10 @@ 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';
+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/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..6c4cd9ca 100644
--- a/run/test/specs/group_tests_add_contact.spec.ts
+++ b/run/test/specs/group_tests_add_contact.spec.ts
@@ -1,10 +1,11 @@
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';
import { newUser } from './utils/create_account';
@@ -33,15 +34,11 @@ 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
- 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));
@@ -51,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
- await alice1.navigateBack(true);
- // iOS doesn't automatically go back to conversation settings
- await alice1.onIOS().navigateBack();
+ // Leave Manage Members
+ await alice1.navigateBack();
+ // Leave Conversation Settings
+ await alice1.navigateBack();
// Check control messages
await Promise.all(
[alice1, bob1, charlie1].map(device =>
@@ -65,7 +61,10 @@ async function addContactToGroup(platform: SupportedPlatformsType) {
)
)
);
+ // 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());
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..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 { EditGroup, EditGroupName } 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,10 +38,10 @@ 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(`groupInformationSet`).toString(),
- englishStrippedStr(`groupNameVisible`).toString()
+ englishStrippedStr(`updateGroupInformation`).toString(),
+ englishStrippedStr(`updateGroupInformationDescription`).toString()
);
await alice1.deleteText(new EditGroupNameInput(alice1));
await alice1.inputText(' ', new EditGroupNameInput(alice1));
@@ -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,15 +84,12 @@ 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));
- // 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));
+ await alice1.clickOnElementAll(new UpdateGroupInformation(alice1));
+ await alice1.clickOnElementAll(new EditGroupNameInput(alice1));
+ await alice1.inputText(newGroupName, new EditGroupNameInput(alice1));
// Click done/apply
- await alice1.clickOnByAccessibilityID('Confirm');
- await alice1.navigateBack(true);
+ await alice1.clickOnElementAll(new SaveGroupNameChangeButton(alice1));
+ 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_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 39191edf..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(
@@ -45,8 +45,8 @@ async function kickMember(platform: SupportedPlatformsType) {
...new GroupMember(alice1).build(USERNAME.BOB),
maxWait: 10000,
});
- await alice1.navigateBack(true);
- await alice1.onIOS().navigateBack();
+ await alice1.navigateBack();
+ await alice1.navigateBack();
await Promise.all([
alice1.waitForControlMessageToBePresent(
englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString()
diff --git a/run/test/specs/group_tests_leave_group.spec.ts b/run/test/specs/group_tests_leave_group.spec.ts
index 11979bdd..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,8 @@
import { englishStrippedStr } from '../../localizer/englishStrippedStr';
import { bothPlatformsIt } from '../../types/sessionIt';
import { ConversationSettings } from './locators/conversation';
-import { LeaveGroupButton } 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';
import { SupportedPlatformsType, closeApp } from './utils/open_app';
@@ -26,9 +27,13 @@ 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.clickOnByAccessibilityID('Leave');
+ 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')
.withArgs({ name: charlie.userName })
@@ -37,9 +42,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/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..e3188a19 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,16 @@ 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/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts
index 2f292171..aaa779cb 100644
--- a/run/test/specs/linked_device_block_user.spec.ts
+++ b/run/test/specs/linked_device_block_user.spec.ts
@@ -1,8 +1,9 @@
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 { ConversationSettings, BlockedBanner } from './locators/conversation';
+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';
@@ -27,32 +28,21 @@ 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({
- 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({
- strategy: 'accessibility id',
- selector: 'Conversation list item',
- text: `${bob.userName}`,
- });
- await alice2.onAndroid().waitForTextElementToBePresent({
- strategy: 'accessibility id',
- selector: 'Blocked banner',
- });
+ await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName));
+ await alice2.onAndroid().waitForTextElementToBePresent(new BlockedBanner(alice2));
console.info(`${bob.userName}` + ' has been blocked');
} else {
console.info('Blocked banner not found');
@@ -66,8 +56,8 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType)
// 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it
await Promise.all([alice1.scrollDown(), alice2.scrollDown()]);
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..71cca47f 100644
--- a/run/test/specs/linked_device_create_group.spec.ts
+++ b/run/test/specs/linked_device_create_group.spec.ts
@@ -1,10 +1,13 @@
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 { EditGroupNameInput } from './locators/groups';
-import { SaveNameChangeButton } from './locators/settings';
+import { ConversationHeaderName, ConversationSettings } from './locators/conversation';
+import {
+ EditGroupNameInput,
+ UpdateGroupInformation,
+ SaveGroupNameChangeButton,
+} from './locators/groups';
+import { ConversationItem } from './locators/home';
import { sleepFor } from './utils';
import { newUser } from './utils/create_account';
import { createGroup } from './utils/create_group';
@@ -35,29 +38,25 @@ 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));
// 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(`groupInformationSet`).toString(),
- englishStrippedStr(`groupNameVisible`).toString()
+ englishStrippedStr('updateGroupInformation').toString(),
+ englishStrippedStr('updateGroupInformationDescription').toString()
);
// Delete old name first
await device1.deleteText(new EditGroupNameInput(device1));
// 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
@@ -69,11 +68,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,16 +93,12 @@ 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
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
@@ -114,8 +107,8 @@ 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.navigateBack(true);
+ await device1.clickOnElementAll(new SaveGroupNameChangeButton(device1));
+ await device1.navigateBack();
// Check control message for changed name
const groupNameNew = englishStrippedStr('groupNameNew')
.withArgs({ group_name: newGroupName })
@@ -123,11 +116,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_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..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,7 @@
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';
import { closeApp, openAppFourDevices, SupportedPlatformsType } from './utils/open_app';
@@ -26,17 +28,11 @@ 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',
- selector: 'Conversation header name',
- text: testGroupName,
- });
+ await device4.waitForTextElementToBePresent(
+ new ConversationHeaderName(device4).build(testGroupName)
+ );
// Check all messages are present
await Promise.all([
device4.waitForTextElementToBePresent({
@@ -57,22 +53,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/linked_group_leave.spec.ts b/run/test/specs/linked_group_leave.spec.ts
index ae90af1b..c8556a7f 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,13 @@ 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
await sleepFor(5000);
await device4.onIOS().hasTextElementBeenDeleted('Conversation list item', testGroupName);
diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts
index 0ef85161..9c49fa4d 100644
--- a/run/test/specs/locators/conversation.ts
+++ b/run/test/specs/locators/conversation.ts
@@ -10,25 +10,18 @@ export class MessageInput extends LocatorsInterface {
}
export class ConversationSettings extends LocatorsInterface {
- public build() {
- return {
- strategy: 'accessibility id',
- selector: 'More options',
- } as const;
- }
-}
-
-// 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',
+ selector: 'conversation-options-avatar',
} as const;
case 'ios':
- throw new Error('Unsupported platform');
+ return {
+ strategy: 'accessibility id',
+ selector: 'More options',
+ } as const;
}
}
}
@@ -93,3 +86,80 @@ 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');
+ }
+ }
+}
+
+export class BlockedBanner extends LocatorsInterface {
+ public build() {
+ switch (this.platform) {
+ case 'android':
+ return {
+ strategy: 'accessibility id',
+ selector: 'blocked-banner',
+ } as const;
+ case 'ios':
+ return {
+ strategy: 'accessibility id',
+ selector: 'Blocked banner',
+ } as const;
+ }
+ }
+}
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/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/locators/groups.ts b/run/test/specs/locators/groups.ts
index 3cf186ff..255f61a0 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 {
@@ -43,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 {
@@ -54,48 +56,77 @@ 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;
}
}
}
-export class LeaveGroupButton extends LocatorsInterface {
+export class LeaveGroupMenuItem extends LocatorsInterface {
public build(): StrategyExtractionObj {
switch (this.platform) {
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 {
@@ -242,3 +280,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',
+ };
+ }
+ }
+}
diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts
index a2f7aef4..2c62fab3 100644
--- a/run/test/specs/locators/index.ts
+++ b/run/test/specs/locators/index.ts
@@ -61,56 +61,6 @@ export class ApplyChanges extends LocatorsInterface {
}
}
-export class EditGroup extends LocatorsInterface {
- public build() {
- switch (this.platform) {
- case 'android':
- return {
- strategy: 'id',
- selector: 'network.loki.messenger:id/title',
- text: 'Edit group',
- } 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: 'Group name',
- } as const;
- }
- }
-}
-
-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) {
@@ -219,13 +169,12 @@ export class BlockUser extends LocatorsInterface {
case 'ios':
return {
strategy: 'accessibility id',
- selector: 'Block - Switch',
+ selector: 'Block',
};
case 'android':
return {
strategy: 'id',
- selector: 'network.loki.messenger:id/title',
- text: 'Block',
+ selector: 'block-user-menu-option',
};
}
}
@@ -319,8 +268,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 {
@@ -336,8 +284,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 {
@@ -353,8 +301,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 {
@@ -382,33 +330,23 @@ export class DeleteMessageConfirmationModal extends LocatorsInterface {
}
}
-export class LeaveGroup extends LocatorsInterface {
+export class BlockUserConfirmationModal extends LocatorsInterface {
public build(): StrategyExtractionObj {
switch (this.platform) {
case 'android':
return {
strategy: 'id',
- selector: `network.loki.messenger:id/title`,
- text: 'Leave group',
- };
+ selector: 'Block',
+ } as const;
case 'ios':
return {
strategy: 'accessibility id',
- selector: 'Leave group',
- };
+ selector: 'Block',
+ } as const;
}
}
}
-export class BlockUserConfirmationModal extends LocatorsInterface {
- public build(): StrategyExtractionObj {
- return {
- strategy: 'accessibility id',
- selector: 'Block',
- } as const;
- }
-}
-
export class BlockedContactsSettings extends LocatorsInterface {
public build(): StrategyExtractionObj {
switch (this.platform) {
@@ -470,23 +408,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..b139591e 100644
--- a/run/test/specs/locators/onboarding.ts
+++ b/run/test/specs/locators/onboarding.ts
@@ -3,21 +3,22 @@ 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',
+ maxWait: 5000,
+ } as const;
+ case 'ios':
+ return {
+ strategy: 'accessibility id',
+ selector: 'Error message',
+ maxWait: 5000,
+ } as const;
+ }
}
}
@@ -50,31 +51,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 +175,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 09e520d4..a520de9c 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;
+ }
}
}
@@ -41,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':
@@ -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,11 +146,45 @@ 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(): StrategyExtractionObj {
+ switch (this.platform) {
+ case 'android':
+ return {
+ strategy: 'id',
+ selector: 'Conversations',
+ } as const;
+ case 'ios':
+ return {
+ strategy: 'accessibility id',
+ selector: 'Conversations',
+ } as const;
+ }
+ }
+}
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/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..8f6acb6b 100644
--- a/run/test/specs/message_community_invitation.spec.ts
+++ b/run/test/specs/message_community_invitation.spec.ts
@@ -7,6 +7,8 @@ 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';
+import { GroupMember } from './locators/groups';
bothPlatformsItSeparate({
title: 'Send community invitation',
@@ -62,11 +64,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);
}
@@ -90,12 +88,11 @@ 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.clickOnElementAll(new GroupMember(alice1).build(bob.userName));
+ await alice1.clickOnElementAll({
+ strategy: 'id',
+ selector: 'invite-contacts-button',
});
- await alice1.clickOnByAccessibilityID('Done');
// Check device 2 for invitation from user A
await bob1.waitForTextElementToBePresent({
strategy: 'id',
@@ -113,14 +110,10 @@ 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();
- 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.spec.ts b/run/test/specs/message_requests_accept.spec.ts
index 6bd018b0..cd24b4fc 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.onAndroid().navigateBack(false);
await Promise.all([
device2.waitForTextElementToBePresent({
strategy: 'accessibility id',
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 8d5ea2fd..da4a0c68 100644
--- a/run/test/specs/message_requests_accept_text_reply.spec.ts
+++ b/run/test/specs/message_requests_accept_text_reply.spec.ts
@@ -2,7 +2,8 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr';
import { bothPlatformsIt } from '../../types/sessionIt';
import { USERNAME } from '../../types/testing';
import { MessageInput } from './locators/conversation';
-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';
@@ -23,14 +24,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 fab57d85..edc0e6ae 100644
--- a/run/test/specs/message_requests_block.spec.ts
+++ b/run/test/specs/message_requests_block.spec.ts
@@ -1,8 +1,9 @@
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 { BlockedContactsSettings } from './locators';
+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';
@@ -41,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([
@@ -62,11 +62,8 @@ 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.navigateBack(false);
+ await device2.waitForTextElementToBePresent(new PlusButton(device2));
// Need to wait to see if message gets through
await sleepFor(5000);
await device2.hasTextElementBeenDeleted('Message body', blockedMessage);
@@ -79,8 +76,8 @@ async function blockedRequest(platform: SupportedPlatformsType) {
// 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it
await Promise.all([device2.scrollDown(), device3.scrollDown()]);
await Promise.all([
- device2.clickOnElementAll({ 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..3c7d4a27 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';
@@ -37,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('messageRequestsDelete').toString(),
- true
- );
+ // 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();
@@ -57,12 +67,9 @@ 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({
- 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/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts
index a690ae91..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('messageRequestsDelete').toString(),
- true
- );
+ // 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/network_page_link_network.spec.ts b/run/test/specs/network_page_link_network.spec.ts
index 8287bc3f..1d605c47 100644
--- a/run/test/specs/network_page_link_network.spec.ts
+++ b/run/test/specs/network_page_link_network.spec.ts
@@ -8,11 +8,10 @@ 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';
-import { verifyPageScreenshot } from './utils/verify_screenshots';
+import { assertUrlIsReachable, ensureHttpsURL } from './utils/utilities';
bothPlatformsIt({
title: 'Network page learn more network link',
@@ -52,7 +51,7 @@ async function networkPageLearnMore(platform: SupportedPlatformsType) {
} else {
console.log('The URLs match.');
}
- await verifyPageScreenshot(platform, device, 'network_page');
+ await assertUrlIsReachable(linkURL);
// 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 13bd5fef..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';
@@ -9,10 +8,10 @@ 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';
+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 5b1be478..4c802acf 100644
--- a/run/test/specs/onboarding_pp.spec.ts
+++ b/run/test/specs/onboarding_pp.spec.ts
@@ -1,9 +1,9 @@
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';
+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 384582f6..cb188cca 100644
--- a/run/test/specs/onboarding_tos.spec.ts
+++ b/run/test/specs/onboarding_tos.spec.ts
@@ -1,9 +1,9 @@
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';
+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/user_actions_block_conversation_list.spec.ts b/run/test/specs/user_actions_block_conversation_list.spec.ts
index fde86a57..df362006 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';
@@ -30,23 +30,21 @@ 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({
- strategy: 'accessibility id',
- selector: 'Blocked banner',
- });
- 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();
- 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 2e611013..da40b025 100644
--- a/run/test/specs/user_actions_block_conversation_options.spec.ts
+++ b/run/test/specs/user_actions_block_conversation_options.spec.ts
@@ -6,8 +6,8 @@ import {
BlockUserConfirmationModal,
ExitUserProfile,
} from './locators';
-import { ConversationSettings } from './locators/conversation';
-import { UserSettings } from './locators/settings';
+import { ConversationSettings, BlockedBanner } from './locators/conversation';
+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';
@@ -36,19 +36,18 @@ 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({
- strategy: 'accessibility id',
- selector: 'Blocked banner',
+ ...new BlockedBanner(alice1).build(),
+ maxWait: 5000,
});
if (blockedStatus) {
console.info(`${bob.userName} has been blocked`);
@@ -60,7 +59,7 @@ async function blockUserInConversationOptions(platform: SupportedPlatformsType)
await alice1.clickOnElementAll(new UserSettings(alice1));
// 'Conversations' might be hidden beyond the Settings view, gotta scroll down to find it
await alice1.scrollDown();
- await alice1.clickOnElementAll({ 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({
@@ -68,8 +67,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_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_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..0c2e7a52 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';
@@ -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/user_actions_share_to_session.spec.ts b/run/test/specs/user_actions_share_to_session.spec.ts
index 2bac498c..68fcc40a 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';
@@ -6,11 +6,19 @@ 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({
+// TODO investigate why the Android Photos app throws an unexpected error when sharing
+bothPlatformsItSeparate({
title: 'Share to session',
risk: 'low',
- testCb: shareToSession,
+ ios: {
+ testCb: shareToSession,
+ },
+ android: {
+ testCb: shareToSession,
+ shouldSkip: true,
+ },
countOfDevicesNeeded: 2,
});
@@ -32,10 +40,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 52c2a2a2..a03902f5 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';
@@ -29,12 +29,11 @@ async function unblockUser(platform: SupportedPlatformsType) {
true
);
await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1));
- await alice1.onIOS().navigateBack();
+ await alice1.navigateBack();
const blockedStatus = await alice1.waitForTextElementToBePresent({
- strategy: 'accessibility id',
- selector: 'Blocked banner',
+ ...new BlockedBanner(alice1).build(),
+ maxWait: 5000,
});
-
if (blockedStatus) {
console.info(`${bob.userName} has been blocked`);
} else {
@@ -49,16 +48,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
+ false
);
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/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts
new file mode 100644
index 00000000..84e7a9b7
--- /dev/null
+++ b/run/test/specs/utils/allure/allureHelpers.ts
@@ -0,0 +1,147 @@
+import fs from 'fs-extra';
+import path from 'path';
+import { execSync } from 'child_process';
+import {
+ allureResultsDir,
+ allureCurrentReportDir,
+ GH_PAGES_BASE_URL,
+} from '../../../../constants/allure';
+import { SupportedPlatformsType } from '../open_app';
+
+export interface ReportContext {
+ platform: SupportedPlatformsType;
+ build: string;
+ artifact: string;
+ risk: string;
+ runNumber: number;
+ runAttempt: number;
+ runID: number;
+ reportFolder: string;
+ reportUrl: string;
+ githubRunUrl: string;
+}
+
+/**
+ * Derives consistent context values from CI env
+ */
+export function getReportContextFromEnv(): ReportContext {
+ 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 = `${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,
+ artifact,
+ risk,
+ runNumber,
+ runAttempt,
+ runID,
+ reportFolder,
+ reportUrl,
+ githubRunUrl,
+ };
+}
+// 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/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 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 executor = {
+ name: 'GitHub Actions',
+ type: 'github',
+ url: ctx.githubRunUrl,
+ buildOrder: buildOrder,
+ buildName: `GitHub Actions Run #${ctx.runID}`,
+ 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..dba572aa 100644
--- a/run/test/specs/utils/allure/closeRun.ts
+++ b/run/test/specs/utils/allure/closeRun.ts
@@ -1,8 +1,11 @@
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,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) => {
@@ -36,15 +26,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 8edcf4ed..5b82f367 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') {
@@ -36,35 +36,21 @@ 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)
- );
+async function publishReport() {
+ const ctx = getReportContextFromEnv();
+ await writeMetadataJson(ctx);
// 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);
+
+ 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) {
@@ -86,9 +72,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) {
@@ -96,15 +82,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}`);
}
}
diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts
index 1681cc10..15fa9c1e 100644
--- a/run/test/specs/utils/create_account.ts
+++ b/run/test/specs/utils/create_account.ts
@@ -2,49 +2,45 @@ 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);
-
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({
- 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();
+ 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/create_group.ts b/run/test/specs/utils/create_group.ts
index b32bbbd5..a2a29b57 100644
--- a/run/test/specs/utils/create_group.ts
+++ b/run/test/specs/utils/create_group.ts
@@ -3,9 +3,12 @@ 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';
+import { sleepFor } from './sleep_for';
export const createGroup = async (
platform: SupportedPlatformsType,
@@ -34,9 +37,9 @@ export const createGroup = async (
// Exit conversation back to list
await device3.navigateBack();
// 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
@@ -44,18 +47,15 @@ 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.clickOnElementAll({
- strategy: 'accessibility id',
- selector: 'Conversation list item',
- text: group.userName,
- }),
- device3.clickOnElementAll({
- strategy: 'accessibility id',
- selector: 'Conversation list item',
- text: group.userName,
- }),
+ 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)),
]);
if (checkControlMessage) {
// Sort by account ID
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/test/specs/utils/join_community.ts b/run/test/specs/utils/join_community.ts
index c2f124fb..1b845d6b 100644
--- a/run/test/specs/utils/join_community.ts
+++ b/run/test/specs/utils/join_community.ts
@@ -1,5 +1,7 @@
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';
export const joinCommunity = async (
@@ -7,13 +9,11 @@ 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));
- await device.waitForTextElementToBePresent({
- strategy: 'accessibility id',
- selector: 'Conversation header name',
- text: communityName,
- });
+ await device.waitForTextElementToBePresent(
+ new ConversationHeaderName(device).build(communityName)
+ );
};
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 870779be..48401eb2 100644
--- a/run/test/specs/utils/restore_account.ts
+++ b/run/test/specs/utils/restore_account.ts
@@ -1,36 +1,35 @@
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';
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);
// 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({
strategy: 'accessibility id',
selector: 'Enter display name',
- maxWait: 1000,
+ maxWait: 2000,
});
if (displayName) {
await device.inputText(user.userName, {
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');
}
@@ -39,15 +38,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));
};
/**
@@ -56,25 +51,22 @@ export const restoreAccount = async (device: DeviceWrapper, user: User) => {
*/
export const restoreAccountNoFallback = async (device: DeviceWrapper, recoveryPhrase: string) => {
await test.step('Restore pre-seeded account', async () => {
- 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({
strategy: 'accessibility id',
selector: 'Enter display name',
- maxWait: 1000,
+ maxWait: 2000,
});
if (displayName) {
throw new Error('Account not found');
@@ -86,14 +78,10 @@ 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..e4d5dbd2 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();
// 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/utils/utilities.ts b/run/test/specs/utils/utilities.ts
index 6c41e628..ce0d1ecf 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}`);
+ }
+}
diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts
index 6f316ac3..8a169616 100644
--- a/run/test/specs/utils/verify_screenshots.ts
+++ b/run/test/specs/utils/verify_screenshots.ts
@@ -8,23 +8,46 @@ import { LocatorsInterfaceScreenshot } from '../locators';
import { SupportedPlatformsType } from './open_app';
import { BrowserPageScreenshot } from './screenshot_paths';
import { cropScreenshot, getDiffDirectory, saveImage } from './utilities';
+import { TestInfo } from '@playwright/test';
-// 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));
+type Attachment = {
+ name: string;
+ body: Buffer | string;
+ contentType: string;
+};
+
+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
@@ -54,18 +77,57 @@ export async function verifyElementScreenshot<
if (!equal) {
const diffImagePath = path.join(diffsDir, `${uuid}_diffImage.png`);
await diffImage.save(diffImagePath);
- 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,
diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts
index 8e8bd9d8..99bb2a97 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';
@@ -28,11 +29,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
@@ -47,7 +44,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',
@@ -72,16 +69,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');
@@ -91,7 +89,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({
@@ -107,13 +105,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
@@ -163,7 +167,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',
@@ -190,10 +194,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',
@@ -221,9 +232,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 f946646b..6ecc2e20 100644
--- a/run/types/DeviceWrapper.ts
+++ b/run/types/DeviceWrapper.ts
@@ -10,13 +10,27 @@ import {
ImageName,
ImagePermissionsModalAllow,
LocatorsInterface,
- PrivacyButton,
ReadReceiptsButton,
SendMediaButton,
} from '../../run/test/specs/locators';
+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';
@@ -33,6 +47,7 @@ import {
User,
XPath,
} from './testing';
+import { PlusButton } from '../test/specs/locators/home';
import {
testFile,
testImage,
@@ -40,8 +55,6 @@ import {
profilePicture,
testVideoThumbnail,
} from '../constants/testfiles';
-import { AttachmentsButton, OutgoingMessageStatusSent } from '../test/specs/locators/conversation';
-import { englishStrippedStr } from '../localizer/englishStrippedStr';
import * as path from 'path';
import fs from 'fs/promises';
import { getImageOccurrence } from '@appium/opencv';
@@ -254,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.
+ */
+ public 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(
+ `[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(`[findWithFallback] Could not find primary or fallback locator`);
+ }
public async longClick(element: AppiumNextElementType, durationMs: number) {
if (this.isIOS()) {
@@ -757,10 +805,34 @@ export class DeviceWrapper {
threshold,
});
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),
- };
+
+ /**
+ * 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);
+ 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);
+
+ const center = { x: tapX, y: tapY };
// If earlyMatch is enabled and the score is high enough, tap immediately
if (earlyMatch && score >= earlyMatchThreshold) {
@@ -1050,8 +1122,7 @@ export class DeviceWrapper {
do {
try {
loadingAnimation = await this.waitForTextElementToBePresent({
- strategy: 'accessibility id',
- selector: 'Loading animation',
+ ...new LoadingAnimation(this).build(),
maxWait: 1000,
});
@@ -1112,17 +1183,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',
@@ -1231,7 +1302,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);
@@ -1283,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
@@ -1343,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` },
@@ -1374,7 +1447,36 @@ 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,
+ });
+ // 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({
+ strategy: 'class name',
+ selector: 'android.widget.Button',
+ text: 'Videos',
+ });
+ // Try again to find the video file after filtering
+ 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 "${testVideo}" not found after attempting to reveal it.`);
+ }
await this.waitForTextElementToBePresent({
...new OutgoingMessageStatusSent(this).build(),
maxWait: 20000,
@@ -1432,7 +1534,36 @@ export class DeviceWrapper {
text: 'Allow',
});
await sleepFor(1000);
- await this.clickOnTextElementById('android:id/title', testFile);
+ let documentElement = await this.doesElementExist({
+ strategy: 'id',
+ selector: 'android:id/title',
+ text: testFile,
+ maxWait: 5000,
+ });
+ // This codepath is purely for the CI
+ if (!documentElement) {
+ // 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 pdf file 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 "${testFile}" not found after attempting to reveal it.`);
+ }
}
// Checking Sent status on both platforms
await this.waitForTextElementToBePresent({
@@ -1527,6 +1658,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 this.matchAndTapImage(
{ strategy: 'xpath', selector: `//XCUIElementTypeImage` },
profilePicture
@@ -1608,7 +1740,7 @@ export class DeviceWrapper {
englishStrippedStr(`attachmentsAutoDownloadModalDescription`)
.withArgs({ conversation_name: conversationName })
.toString(),
- true
+ false
);
await this.clickOnElementAll(new DownloadMediaButton(this));
}
@@ -1724,27 +1856,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);
}
}
@@ -1763,12 +1917,12 @@ 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();
+ await this.navigateBack(false);
await sleepFor(100);
- await this.closeScreen();
+ await this.closeScreen(false);
}
public async checkPermissions(
@@ -1870,50 +2024,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 1b939f1d..8e5dae0b 100644
--- a/run/types/testing.ts
+++ b/run/types/testing.ts
@@ -80,6 +80,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`,
@@ -139,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'
@@ -233,7 +236,6 @@ export type AccessibilityId =
| 'Confirm delete'
| 'Delete'
| 'Block'
- | 'Block - Switch'
| 'Unblock'
| 'Confirm block'
| 'Blocked contacts'
@@ -344,12 +346,14 @@ export type AccessibilityId =
| 'Legacy group banner'
| 'Legacy Groups Recreate Button'
| 'Confirm leave'
+ | 'Albums'
+ | `Disappear after ${DisappearActions} option`
+ | 'Call button'
| 'Session Network'
| 'Learn more link'
| 'Open'
| 'Learn about staking link'
| 'Last updated timestamp'
- | 'Albums'
| 'Save to Files'
| 'Replace'
| 'ShareButton'
@@ -359,7 +363,10 @@ export type AccessibilityId =
| 'Appearance'
| 'Select alternate app icon'
| 'MeetingSE'
- | 'Donate';
+ | 'Donate'
+ | 'blocked-banner'
+ | 'Manage Members'
+ | `${GROUPNAME}`;
export type Id =
| 'Modal heading'
@@ -395,7 +402,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'
@@ -433,7 +439,53 @@ export type Id =
| 'Image button'
| 'network.loki.messenger:id/system_settings_app_icon'
| 'MeetingSE option'
- | 'donate-menu-item';
+ | 'donate-menu-item'
+ | '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-menu-option'
+ | 'block-user-confirm-button'
+ | 'Notifications'
+ | 'All Session notifications'
+ | 'com.android.settings:id/switch_text'
+ | 'Block'
+ | 'invite-contacts-menu-option'
+ | 'invite-contacts-button'
+ | 'Recovery password menu item'
+ | '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'
+ | 'group-name'
+ | 'leave-group-menu-option'
+ | 'Leave'
+ | 'Appearance';
export type TestRisk = 'high' | 'medium' | 'low';
diff --git a/scripts/ci.sh b/scripts/ci.sh
index dac3a18c..647c403e 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 4GB (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
@@ -87,13 +87,31 @@ 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"
+ for i in {1..4}; do
+ EMU_CONFIG_FILE="$HOME/.android/avd/emulator$i.avd/emulator-user.ini"
+ # 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 &
+
+ sleep 5
+ done
+}
- DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load &
- sleep 5
+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
}
+
+
+set +x
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 5f666975..51f22b53 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1767,7 +1767,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=4b0b2d"
dependencies:
appium-adb: "npm:^12.7.0"
appium-android-driver: "npm:^9.12.3"
@@ -1784,7 +1784,7 @@ __metadata:
type-fest: "npm:^4.4.0"
peerDependencies:
appium: ^2.4.1
- checksum: 10c0/a026f90c2089a1c4fd7a8667d0b19999aa60297b36480d79d8056e8dd80e5da1177cd992ccc029350131ff2f442d9686136ce10c0e2ef1dcc175ea0b9a4e8bfb
+ checksum: 10c0/51fbe71a9054192e867f57126f4f09087713681bb97cf2ed47953460bda6f2451d560e45669ff5fcee34fbbb368f59f883899db57d972f9318a2daad8d996044
languageName: node
linkType: hard