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