From d2722d795d2e99d4140f83473c65feabf4a530df Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 5 Jan 2026 10:44:35 +1100 Subject: [PATCH 001/184] feat: groups banners now check non existence --- run/test/specs/group_tests_create_group_banner.spec.ts | 9 ++++----- run/test/specs/group_tests_edit_group_banner.spec.ts | 10 +++++----- .../specs/group_tests_invite_contact_banner.spec.ts | 7 +++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/run/test/specs/group_tests_create_group_banner.spec.ts b/run/test/specs/group_tests_create_group_banner.spec.ts index a5f57c4a8..21c6b96ab 100644 --- a/run/test/specs/group_tests_create_group_banner.spec.ts +++ b/run/test/specs/group_tests_create_group_banner.spec.ts @@ -1,14 +1,13 @@ import type { TestInfo } from '@playwright/test'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { LatestReleaseBanner } from './locators/groups'; import { PlusButton } from './locators/home'; import { CreateGroupOption } from './locators/start_conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// This banner no longer exists on iOS -androidIt({ +bothPlatformsIt({ title: 'Create group banner', risk: 'high', testCb: createGroupBanner, @@ -18,7 +17,7 @@ androidIt({ suite: 'Create Group', }, allureDescription: - 'Verifies that the latest release banner is present on the Create Group screen', + 'Verifies that the latest release banner is no longer present on the Create Group screen', }); async function createGroupBanner(platform: SupportedPlatformsType, testInfo: TestInfo) { @@ -34,6 +33,6 @@ async function createGroupBanner(platform: SupportedPlatformsType, testInfo: Tes await alice1.clickOnElementAll(new PlusButton(alice1)); await alice1.clickOnElementAll(new CreateGroupOption(alice1)); // Verify the banner is present - await alice1.waitForTextElementToBePresent(new LatestReleaseBanner(alice1)); + await alice1.verifyElementNotPresent(new LatestReleaseBanner(alice1)); await closeApp(alice1, bob1); } 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 0e9d55c69..848728f79 100644 --- a/run/test/specs/group_tests_edit_group_banner.spec.ts +++ b/run/test/specs/group_tests_edit_group_banner.spec.ts @@ -1,13 +1,12 @@ import type { TestInfo } from '@playwright/test'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationSettings } from './locators/conversation'; import { LatestReleaseBanner, ManageMembersMenuItem } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// This banner no longer exists on iOS -androidIt({ +bothPlatformsIt({ title: 'Edit group banner', risk: 'medium', testCb: editGroupBanner, @@ -16,7 +15,8 @@ androidIt({ parent: 'Groups', suite: 'Edit Group', }, - allureDescription: 'Verifies that the latest release banner is present on the Edit Group screen', + allureDescription: + 'Verifies that the latest release banner is no longer present on the Edit Group screen', }); async function editGroupBanner(platform: SupportedPlatformsType, testInfo: TestInfo) { @@ -33,6 +33,6 @@ async function editGroupBanner(platform: SupportedPlatformsType, testInfo: TestI // Navigate to Edit Group screen await alice1.clickOnElementAll(new ConversationSettings(alice1)); await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); - await alice1.waitForTextElementToBePresent(new LatestReleaseBanner(alice1)); + await alice1.verifyElementNotPresent(new LatestReleaseBanner(alice1)); await closeApp(alice1, bob1, charlie1); } 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 a6bd351e4..43112fa3b 100644 --- a/run/test/specs/group_tests_invite_contact_banner.spec.ts +++ b/run/test/specs/group_tests_invite_contact_banner.spec.ts @@ -1,14 +1,13 @@ import type { TestInfo } from '@playwright/test'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { InviteContactsButton } from './locators'; import { ConversationSettings } from './locators/conversation'; import { LatestReleaseBanner, ManageMembersMenuItem } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -// This banner no longer exists on iOS -androidIt({ +bothPlatformsIt({ title: 'Invite contacts banner', risk: 'medium', testCb: inviteContactGroupBanner, @@ -36,6 +35,6 @@ async function inviteContactGroupBanner(platform: SupportedPlatformsType, testIn await alice1.clickOnElementAll(new ConversationSettings(alice1)); await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); await alice1.clickOnElementAll(new InviteContactsButton(alice1)); - await alice1.waitForTextElementToBePresent(new LatestReleaseBanner(alice1)); + await alice1.verifyElementNotPresent(new LatestReleaseBanner(alice1)); await closeApp(alice1, bob1, charlie1); } From a42590a429efacdb4b55e043ea982c6867fc0f1d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 5 Jan 2026 11:34:03 +1100 Subject: [PATCH 002/184] feat: add kick and remove messages test --- .../group_tests_kick_member_messages.spec.ts | 100 ++++++++++++++++++ run/test/specs/locators/groups.ts | 2 +- run/types/testing.ts | 1 + 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 run/test/specs/group_tests_kick_member_messages.spec.ts diff --git a/run/test/specs/group_tests_kick_member_messages.spec.ts b/run/test/specs/group_tests_kick_member_messages.spec.ts new file mode 100644 index 000000000..12d1d4b5d --- /dev/null +++ b/run/test/specs/group_tests_kick_member_messages.spec.ts @@ -0,0 +1,100 @@ +import type { TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { androidIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { + ConversationSettings, + DeletedMessage, + MessageBody, + MessageInput, +} from './locators/conversation'; +import { + ConfirmRemovalButton, + GroupMember, + ManageMembersMenuItem, + RemoveMemberButton, +} from './locators/groups'; +import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { SupportedPlatformsType } from './utils/open_app'; + +// This functionality only exists on Android at the moment +androidIt({ + title: 'Kick and remove messages', + risk: 'medium', + testCb: kickMember, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: + 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group.', +}); + +async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Kick member'; + + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { alice, bob }, + } = await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + const aliceMsg = `Hello I am ${alice.userName}`; + const bobMsg = `Hello I am ${bob.userName}`; + await alice1.sendMessage(aliceMsg); + await bob1.sendMessage(bobMsg); + await alice1.clickOnElementAll(new ConversationSettings(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( + englishStrippedStr('remove').toString(), + englishStrippedStr('groupRemoveDescription') + .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) + .toString() + ); + await alice1.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('removeMemberMessages').withArgs({ count: 1 }).toString()}")`, + }); + await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + await alice1.verifyElementNotPresent({ + ...new GroupMember(alice1).build(USERNAME.BOB), + maxWait: 5_000, + }); + await alice1.navigateBack(); + await alice1.navigateBack(); + await Promise.all( + [alice1, charlie1].map(async device => { + await device.waitForControlMessageToBePresent( + englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() + ); + await device.waitForTextElementToBePresent(new MessageBody(device, aliceMsg)); + await device.verifyElementNotPresent({ + ...new MessageBody(device, bobMsg).build(), + maxWait: 1_000, + }); + await device.waitForTextElementToBePresent(new DeletedMessage(device)); + }) + ); + await Promise.all([ + bob1.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'Empty list', + text: englishStrippedStr('groupRemovedYou') + .withArgs({ group_name: testGroupName }) + .toString(), + }), + bob1.verifyElementNotPresent(new MessageBody(bob1, aliceMsg)), + bob1.verifyElementNotPresent(new MessageBody(bob1, bobMsg)), + bob1.verifyElementNotPresent(new DeletedMessage(bob1)), + bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1_000 }), + ]); +} diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 19b2a9e6a..17e3f2841 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -310,7 +310,7 @@ export class RemoveMemberButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Remove contact button', + selector: 'qa-collapsing-footer-action_remove', } as const; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index 904a42125..383ca6481 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -561,6 +561,7 @@ export type Id = | 'Privacy policy button' | 'pro-badge-text' | 'qa-collapsing-footer-action_invite' + | 'qa-collapsing-footer-action_remove' | 'Quit' | 'rate-app-button' | 'Recovery password container' From 6f86bb6a9619fdd3e0b2104bf7c7f3902c3f0065 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 6 Jan 2026 17:12:28 +1100 Subject: [PATCH 003/184] fix: new android workflow for add contact --- run/test/specs/group_tests_add_contact.spec.ts | 10 +++++++++- run/types/testing.ts | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index ce4cecae8..99a935099 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -50,13 +50,21 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes await sleepFor(1000); // Add contact to group await alice1.onIOS().clickOnElementAll(new InviteContactsMenuItem(alice1)); - await alice1.onAndroid().clickOnElementAll(new InviteContactsButton(alice1)); + // await alice1.onAndroid().clickOnElementAll(new InviteContactsButton(alice1)); // This is temporarily broken, SES-5049 + await alice1.onAndroid().clickOnElementAll({ + strategy: '-android uiautomator', + selector: 'new UiSelector().text("Invite Contacts")' + }) // Select new user await alice1.clickOnElementAll({ ...new Contact(alice1).build(), text: USERNAME.DRACULA, }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); + await alice1.onAndroid().clickOnElementAll({ + strategy: 'id', + selector: 'Send Invite' + }); // Leave Manage Members await alice1.navigateBack(); // Leave Conversation Settings diff --git a/run/types/testing.ts b/run/types/testing.ts index 383ca6481..cdbd08d2d 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -573,6 +573,7 @@ export type Id = | 'Reveal recovery phrase button' | 'Save' | 'Select All' + | 'Send Invite' | 'SESH price' | 'session-network-menu-item' | 'Session id input box' From b0f39492c5a38f7c00ea8f21dfb9fcaf31ea0c2e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 6 Jan 2026 17:28:36 +1100 Subject: [PATCH 004/184] fix: add uiscrollable for delete group --- run/test/specs/locators/groups.ts | 4 ++-- run/types/testing.ts | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 17e3f2841..54de55371 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -201,8 +201,8 @@ export class DeleteGroupMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'delete-group-menu-option', + strategy: '-android uiautomator', + selector: 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("delete-group-menu-option"))' } as const; case 'ios': return { diff --git a/run/types/testing.ts b/run/types/testing.ts index cdbd08d2d..e374ed06d 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -159,15 +159,13 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; export type UiAutomatorQuery = - | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Appearance"))' - | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Conversations"))' - | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("path-menu-item"))' - | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Select app icon"))' - | 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith("Version"))' | 'new UiSelector().resourceId("cta-button-negative").childSelector(new UiSelector().className("android.widget.TextView"))' | 'new UiSelector().resourceId("cta-button-positive").childSelector(new UiSelector().className("android.widget.TextView"))' | 'new UiSelector().resourceId("network.loki.messenger:id/messageStatusTextView").text("Sent")' | 'new UiSelector().text("Enter your display name")' + | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId(${string}))` + | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text(${string}))` + | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith(${string}))` | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` | `new UiSelector().text(${string})`; From 4abcf7fa65869ccc7155f01c4ebd9220d4021f86 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 7 Jan 2026 16:11:30 +1100 Subject: [PATCH 005/184] fix: use proper locator for invite contact --- run/test/specs/group_tests_add_contact.spec.ts | 13 ++++--------- .../group_tests_invite_contact_banner.spec.ts | 4 ++-- run/test/specs/locators/groups.ts | 3 ++- run/test/specs/locators/index.ts | 17 ----------------- 4 files changed, 8 insertions(+), 29 deletions(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 99a935099..7d90dd1dc 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { InviteContactsButton, InviteContactsMenuItem } from './locators'; +import { InviteContactsMenuItem } from './locators'; import { ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; import { InviteContactConfirm, ManageMembersMenuItem } from './locators/groups'; @@ -49,12 +49,7 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); await sleepFor(1000); // Add contact to group - await alice1.onIOS().clickOnElementAll(new InviteContactsMenuItem(alice1)); - // await alice1.onAndroid().clickOnElementAll(new InviteContactsButton(alice1)); // This is temporarily broken, SES-5049 - await alice1.onAndroid().clickOnElementAll({ - strategy: '-android uiautomator', - selector: 'new UiSelector().text("Invite Contacts")' - }) + await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); // Select new user await alice1.clickOnElementAll({ ...new Contact(alice1).build(), @@ -62,8 +57,8 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); await alice1.onAndroid().clickOnElementAll({ - strategy: 'id', - selector: 'Send Invite' + strategy: 'id', + selector: 'Send Invite', }); // Leave Manage Members await alice1.navigateBack(); 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 43112fa3b..2f5156214 100644 --- a/run/test/specs/group_tests_invite_contact_banner.spec.ts +++ b/run/test/specs/group_tests_invite_contact_banner.spec.ts @@ -1,7 +1,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { InviteContactsButton } from './locators'; +import { InviteContactsMenuItem } from './locators'; import { ConversationSettings } from './locators/conversation'; import { LatestReleaseBanner, ManageMembersMenuItem } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; @@ -34,7 +34,7 @@ async function inviteContactGroupBanner(platform: SupportedPlatformsType, testIn // Navigate to Invite Contacts screen await alice1.clickOnElementAll(new ConversationSettings(alice1)); await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); - await alice1.clickOnElementAll(new InviteContactsButton(alice1)); + await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); await alice1.verifyElementNotPresent(new LatestReleaseBanner(alice1)); await closeApp(alice1, bob1, charlie1); } diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 54de55371..dd2dd8898 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -202,7 +202,8 @@ export class DeleteGroupMenuItem extends LocatorsInterface { case 'android': return { strategy: '-android uiautomator', - selector: 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("delete-group-menu-option"))' + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("delete-group-menu-option"))', } as const; case 'ios': return { diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 960e974ba..847209c85 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -270,23 +270,6 @@ export class CommunityInput extends LocatorsInterface { } } -export class InviteContactsButton extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Invite button', - }; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Invite button', - }; - } - } -} - export class InviteContactsMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { From 26538f6c5b3fb5c4fc7d73c882547ecd5cb43247 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 8 Jan 2026 14:47:18 +1100 Subject: [PATCH 006/184] feat: add message history and invite account id tests --- .../specs/group_tests_add_accountid.spec.ts | 84 ++++++++++++++++ .../specs/group_tests_add_contact.spec.ts | 29 ++++-- .../group_tests_add_contact_nohistory.spec.ts | 97 +++++++++++++++++++ run/test/specs/locators/index.ts | 14 +++ run/test/specs/utils/create_contact.ts | 2 +- run/types/testing.ts | 1 + 6 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 run/test/specs/group_tests_add_accountid.spec.ts create mode 100644 run/test/specs/group_tests_add_contact_nohistory.spec.ts diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts new file mode 100644 index 000000000..7901d7c89 --- /dev/null +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -0,0 +1,84 @@ +import type { TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { androidIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { InviteAccountIDOrONS } from './locators'; +import { ConversationSettings, MessageBody } from './locators/conversation'; +import { ManageMembersMenuItem } from './locators/groups'; +import { ConversationItem, MessageRequestsBanner } from './locators/home'; +import { EnterAccountID, NextButton } from './locators/start_conversation'; +import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; +import { sleepFor } from './utils'; +import { newUser } from './utils/create_account'; +import { truncatePubkey } from './utils/get_account_id'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +androidIt({ + title: 'Invite Account ID to group', + risk: 'high', + testCb: addContactToGroup, + countOfDevicesNeeded: 4, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: + 'Verifies that inviting a non-contact Account ID (without chat history) works as expected.', +}); +async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Group to test adding contact'; + const { + devices: { alice1, bob1, charlie1, unknown1 }, + prebuilt: { alice, group }, + } = await open_Alice1_Bob1_Charlie1_Unknown1({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo: testInfo, + }); + const historicMsg = `Hello from ${alice.userName}`; + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); + const userD = await newUser(unknown1, USERNAME.DRACULA); + const userDTruncatedPubkey = truncatePubkey(userD.accountID, platform); + // Click more options + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + // Select edit group + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await sleepFor(1000); + // Add contact to group + await alice1.clickOnElementAll(new InviteAccountIDOrONS(alice1)); + await alice1.inputText(userD.accountID, new EnterAccountID(alice1)); + await alice1.clickOnElementAll(new NextButton(alice1)); + await alice1.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareNewMessagesOnly').toString()}")`, + }); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'Send Invite', + }); + // Leave Manage Members + await alice1.navigateBack(); + // Leave Conversation Settings + await alice1.navigateBack(); + // Check control messages + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberNew').withArgs({ name: userDTruncatedPubkey }).toString(), + 20_000 + ) + ) + ); + await unknown1.clickOnElementAll(new MessageRequestsBanner(unknown1)); + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); + await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); + await closeApp(alice1, bob1, charlie1, unknown1); +} diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 7d90dd1dc..29195462f 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -1,10 +1,10 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; -import { bothPlatformsIt } from '../../types/sessionIt'; +import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteContactsMenuItem } from './locators'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; import { InviteContactConfirm, ManageMembersMenuItem } from './locators/groups'; import { ConversationItem } from './locators/home'; @@ -14,8 +14,8 @@ import { newUser } from './utils/create_account'; import { newContact } from './utils/create_contact'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; -bothPlatformsIt({ - title: 'Add contact to group', +androidIt({ + title: 'Invite contact to group with chat history', risk: 'high', testCb: addContactToGroup, countOfDevicesNeeded: 4, @@ -23,7 +23,8 @@ bothPlatformsIt({ parent: 'Groups', suite: 'Edit Group', }, - allureDescription: 'Create four accounts, create a group with three, add the fourth member', + allureDescription: + 'Verifies that inviting a contact to a group with message history works as expected.', }); async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Group to test adding contact'; @@ -36,6 +37,13 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes focusGroupConvo: true, testInfo: testInfo, }); + const historicMsg = `Hello from ${alice.userName}`; + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); const userD = await newUser(unknown1, USERNAME.DRACULA); await alice1.navigateBack(); await newContact(platform, alice1, alice, unknown1, userD); @@ -56,7 +64,11 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes text: USERNAME.DRACULA, }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); - await alice1.onAndroid().clickOnElementAll({ + await alice1.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareMessageHistoryDays').toString()}")`, + }); + await alice1.clickOnElementAll({ strategy: 'id', selector: 'Send Invite', }); @@ -74,9 +86,10 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes ); // Leave conversation await unknown1.navigateBack(); - // Leave Message Requests screen (Android) - await unknown1.onAndroid().navigateBack(); + // Leave Message Requests screen + await unknown1.navigateBack(); await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 + await unknown1.waitForTextElementToBePresent(new MessageBody(unknown1, historicMsg)); await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); await closeApp(alice1, bob1, charlie1, unknown1); } diff --git a/run/test/specs/group_tests_add_contact_nohistory.spec.ts b/run/test/specs/group_tests_add_contact_nohistory.spec.ts new file mode 100644 index 000000000..5ec74938a --- /dev/null +++ b/run/test/specs/group_tests_add_contact_nohistory.spec.ts @@ -0,0 +1,97 @@ +import type { TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { InviteContactsMenuItem } from './locators'; +import { ConversationSettings, MessageBody } from './locators/conversation'; +import { Contact } from './locators/global'; +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'; +import { newContact } from './utils/create_contact'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +bothPlatformsIt({ + title: 'Invite contact to group without chat history', + risk: 'high', + testCb: addContactToGroup, + countOfDevicesNeeded: 4, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: + 'Verifies that inviting a contact (Android: without chat history) works as expected.', +}); +async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Group to test adding contact'; + const { + devices: { alice1, bob1, charlie1, unknown1 }, + prebuilt: { alice, group }, + } = await open_Alice1_Bob1_Charlie1_Unknown1({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo: testInfo, + }); + const historicMsg = `Hello from ${alice.userName}`; + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); + const userD = await newUser(unknown1, USERNAME.DRACULA); + await alice1.navigateBack(); + await newContact(platform, alice1, alice, unknown1, userD); + // Exit to conversation list + await alice1.navigateBack(); + // Select group conversation in list + await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); + // Click more options + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + // Select edit group + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await sleepFor(1000); + // Add contact to group + await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); + // Select new user + await alice1.clickOnElementAll({ + ...new Contact(alice1).build(), + text: USERNAME.DRACULA, + }); + await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); + if (platform === 'android') { + await alice1.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareNewMessagesOnly').toString()}")`, + }); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'Send Invite', + }); + } + // Leave Manage Members + await alice1.navigateBack(); + // Leave Conversation Settings + await alice1.navigateBack(); + // Check control messages + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberNew').withArgs({ name: USERNAME.DRACULA }).toString() + ) + ) + ); + // Leave conversation + await unknown1.navigateBack(); + // Leave Message Requests screen (Android) + await unknown1.onAndroid().navigateBack(); + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 + await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); + await closeApp(alice1, bob1, charlie1, unknown1); +} diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 791b870e0..3dd284f78 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -304,6 +304,20 @@ export class InviteContactsMenuItem extends LocatorsInterface { } } +export class InviteAccountIDOrONS extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'invite-accountid-menu-option', + } as const; + case 'ios': + throw new Error('Manage Members not implemented yet on iOS'); + } + } +} + export class DeleteMessageLocally extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/specs/utils/create_contact.ts b/run/test/specs/utils/create_contact.ts index eeba536f2..0316eacbf 100644 --- a/run/test/specs/utils/create_contact.ts +++ b/run/test/specs/utils/create_contact.ts @@ -21,7 +21,7 @@ export const newContact = async ( await device2.clickOnByAccessibilityID('Message request'); await device2.onAndroid().clickOnByAccessibilityID('Accept message request'); // Type into message input box - const replyMessage = `Reply-message-${receiver.userName}-to-${sender.userName}`; + const replyMessage = `${receiver.userName} to ${sender.userName}`; await device2.sendMessage(replyMessage); // Verify config message states message request was accepted diff --git a/run/types/testing.ts b/run/types/testing.ts index a4395a5c3..c71c5f7f3 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -500,6 +500,7 @@ export type Id = | 'Hide recovery password button' | 'Image button' | 'Image picker' + | 'invite-accountid-menu-option' | 'invite-contacts-menu-option' | 'Invite button' | 'Invite friend button' From 063ee595e569bac7cbdd64759fb9bee373d734e2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 8 Jan 2026 15:27:16 +1100 Subject: [PATCH 007/184] Merge origin/dev --- eslint.config.mjs | 6 + run/test/specs/locators/browsers.ts | 35 +- run/test/specs/locators/conversation.ts | 534 +++++++++--------- .../specs/locators/disappearing_messages.ts | 82 +-- run/test/specs/locators/external.ts | 42 +- run/test/specs/locators/global.ts | 158 +++--- run/test/specs/locators/global_search.ts | 16 +- run/test/specs/locators/groups.ts | 202 +++---- run/test/specs/locators/home.ts | 100 ++-- run/test/specs/locators/index.ts | 359 ++++++------ run/test/specs/locators/network_page.ts | 96 ++-- run/test/specs/locators/onboarding.ts | 103 ++-- run/test/specs/locators/settings.ts | 216 +++---- run/test/specs/locators/start_conversation.ts | 67 ++- 14 files changed, 1012 insertions(+), 1004 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index ef4b1204b..a708d9643 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -89,5 +89,11 @@ export default tseslint.config( 'perfectionist/sort-named-imports': 'off', 'perfectionist/sort-union-types': 'off', }, + }, + { + files: ['run/test/specs/locators/*'], + rules: { + 'perfectionist/sort-modules': 'error', + }, } ); diff --git a/run/test/specs/locators/browsers.ts b/run/test/specs/locators/browsers.ts index e73ed35e0..14039f64a 100644 --- a/run/test/specs/locators/browsers.ts +++ b/run/test/specs/locators/browsers.ts @@ -1,24 +1,19 @@ import { LocatorsInterface } from '.'; -// SHARED LOCATORS -export class URLInputField extends LocatorsInterface { +export class ChromeNotificationsNegativeButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'com.android.chrome:id/url_bar', + selector: 'com.android.chrome:id/negative_button', } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'URL', - } as const; + throw new Error('Unsupported platform'); } } } -// ANDROID ONLY export class ChromeUseWithoutAnAccount extends LocatorsInterface { public build() { switch (this.platform) { @@ -34,22 +29,21 @@ export class ChromeUseWithoutAnAccount extends LocatorsInterface { } } -export class ChromeNotificationsNegativeButton extends LocatorsInterface { +export class SafariAddressBar extends LocatorsInterface { public build() { switch (this.platform) { case 'android': + throw new Error('Unsupported platform'); + case 'ios': return { - strategy: 'id', - selector: 'com.android.chrome:id/negative_button', + strategy: 'accessibility id', + selector: 'TabBarItemTitle', } as const; - case 'ios': - throw new Error('Unsupported platform'); } } } -// iOS ONLY -export class SafariAddressBar extends LocatorsInterface { +export class SafariShareButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': @@ -57,20 +51,23 @@ export class SafariAddressBar extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'TabBarItemTitle', + selector: 'ShareButton', } as const; } } } -export class SafariShareButton extends LocatorsInterface { +export class URLInputField extends LocatorsInterface { public build() { switch (this.platform) { case 'android': - throw new Error('Unsupported platform'); + return { + strategy: 'id', + selector: 'com.android.chrome:id/url_bar', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'ShareButton', + selector: 'URL', } as const; } } diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 60dc4979a..8018d14ea 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -6,181 +6,234 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { getAppDisplayName } from '../utils/devnet'; import { LocatorsInterface } from './index'; -export class MessageInput extends LocatorsInterface { +export class AttachmentsButton extends LocatorsInterface { public build() { return { strategy: 'accessibility id', - selector: 'Message input box', + selector: 'Attachments button', } as const; } } -export class SendButton extends LocatorsInterface { - public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Send message button', - }; +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; + } } } -export class NewVoiceMessageButton extends LocatorsInterface { +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: 'New voice message', + selector: 'Call', } as const; } } } -export class MessageBody extends LocatorsInterface { - public text: string | undefined; - constructor(device: DeviceWrapper, text?: string) { - super(device); - this.text = text; - } - public build() { +export class CommunityInvitation extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/openGroupTitleTextView', + text: testCommunityName, + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Message body', - text: this.text, + selector: 'Community invitation', + text: testCommunityName, } as const; } } } -export class VoiceMessage extends LocatorsInterface { +export class CommunityInviteConfirmButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'qa-collapsing-footer-action_invite', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Voice message', + selector: 'Invite contacts button', } as const; } } } -export class MediaMessage extends LocatorsInterface { - public build() { +export class CommunityMessageAuthor extends LocatorsInterface { + public text: string; + constructor(device: DeviceWrapper, text: string) { + super(device); + this.text = text; + } + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': + // Identify the profile picture of a message with a specific text + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id='network.loki.messenger:id/mainContainer'][.//android.widget.TextView[contains(@text,'${this.text}')]]//androidx.compose.ui.platform.ComposeView[@resource-id='network.loki.messenger:id/profilePictureView']`, + } as const; case 'ios': + // Identify the display name of a blinded sender of a message with a specific text return { - strategy: 'accessibility id', - selector: 'Media message', + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@name='Message body' and contains(@label,'${this.text}')]]//XCUIElementTypeStaticText[contains(@value,'(15')]`, } as const; } } } -export class DocumentMessage extends LocatorsInterface { +export class ConversationHeaderName extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': + return { + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))`, + text: this.text, + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Document', + selector: 'Conversation header name', + text: this.text, } as const; } } } -export class ScrollToBottomButton extends LocatorsInterface { +export class ConversationSettings extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/scrollToBottomButton', + selector: 'conversation-options-avatar', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Scroll button', + selector: 'More options', } as const; } } } -export class ConversationSettings extends LocatorsInterface { +export class DeleteContactConfirmButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'conversation-options-avatar', + selector: 'delete-contact-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'More options', + selector: 'Delete', } as const; } } } -export class DeletedMessage extends LocatorsInterface { +export class DeleteContactMenuItem extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'Deleted message', - } as const; + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'delete-contact-menu-option', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Delete Contact', + } as const; + } } } -// Empty conversation state -export class EmptyConversation extends LocatorsInterface { +export class DeleteConversationMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'delete-conversation-menu-option', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Empty list', + selector: 'Delete Conversation', } as const; } } } -export class Hide extends LocatorsInterface { +export class DeleteConversationModalConfirm extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Clear', // I guess they changed the label to Hide but not the ax id + strategy: 'id', + selector: 'delete-conversation-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Hide', + selector: 'Delete', } as const; } } } -export class AttachmentsButton extends LocatorsInterface { +export class DeletedMessage extends LocatorsInterface { public build() { return { strategy: 'accessibility id', - selector: 'Attachments button', + selector: 'Deleted message', } as const; } } -export class GIFButton extends LocatorsInterface { +export class DocumentMessage extends LocatorsInterface { public build() { - return { - strategy: 'accessibility id', - selector: 'GIF button', - } as const; + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Document', + } as const; + } } } @@ -193,335 +246,363 @@ export class DocumentsFolderButton extends LocatorsInterface { } } -export class ImagesFolderButton extends LocatorsInterface { - public build() { - return { - strategy: 'accessibility id', - selector: 'Images folder', - } as const; - } -} - -// TODO tie this to the message whose status we want to check (similar to EmojiReactsPill) -export class OutgoingMessageStatusSent extends LocatorsInterface { +export class EditNicknameButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'network.loki.messenger:id/messageStatusTextView', - text: 'Sent', + strategy: 'accessibility id', + selector: 'Edit', } as const; case 'ios': return { strategy: 'accessibility id', - selector: `Message sent status: Sent`, + selector: 'Username', } as const; } } } -export class CallButton extends LocatorsInterface { - public build() { +export class EmojiReactsCount extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private messageText: string, + private expectedCount: string = '2' + ) { + super(device); + } + + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Call button', + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger:id/reactions_pill_count"][@text="${this.expectedCount}"]`, } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Call', + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]`, } as const; } } } -export class ConversationHeaderName extends LocatorsInterface { - public text: string | undefined; - constructor(device: DeviceWrapper, text?: string) { +// Find the reactions pill underneath a specific message +export class EmojiReactsPill extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private messageText: string + ) { super(device); - this.text = text; } - public build() { + + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))`, - text: this.text, + strategy: 'xpath', + selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger:id/layout_emoji_container"]`, } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Conversation header name', - text: this.text, + strategy: 'xpath', + selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]`, } as const; } } } -export class NotificationsModalButton extends LocatorsInterface { +// Empty conversation state +export class EmptyConversation extends LocatorsInterface { public build() { switch (this.platform) { case 'android': case 'ios': return { strategy: 'accessibility id', - selector: 'Notifications', + selector: 'Empty list', } as const; } } } -export class NotificationSwitch extends LocatorsInterface { +export class FirstEmojiReact extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'com.android.settings:id/switch_text', - text: `All ${getAppDisplayName()} notifications`, + selector: 'network.loki.messenger:id/reaction_1', } as const; case 'ios': - throw new Error('Platform not supported'); + return { + strategy: 'accessibility id', + selector: '😂', + } as const; } } } -export class BlockedBanner extends LocatorsInterface { +export class GIFButton extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'GIF button', + } as const; + } +} + +export class Hide extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'accessibility id', - selector: 'blocked-banner', + selector: 'Clear', // I guess they changed the label to Hide but not the ax id } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Blocked banner', + selector: 'Hide', } as const; } } } -export class DeleteConversationMenuItem extends LocatorsInterface { +export class HideNoteToSelfConfirmButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'delete-conversation-menu-option', + selector: 'hide-nts-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete Conversation', + selector: 'Hide', } as const; } } } -export class DeleteConversationModalConfirm extends LocatorsInterface { +export class HideNoteToSelfMenuOption extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'delete-conversation-confirm-button', + selector: 'hide-nts-menu-option', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete', + selector: 'Hide Note to Self', } as const; } } } -export class HideNoteToSelfMenuOption extends LocatorsInterface { +export class ImagesFolderButton extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'Images folder', + } as const; + } +} + +export class MediaMessage extends LocatorsInterface { public build() { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'hide-nts-menu-option', - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Hide Note to Self', + selector: 'Media message', } as const; } } } -export class HideNoteToSelfConfirmButton extends LocatorsInterface { +export class MessageBody extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'hide-nts-confirm-button', - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Hide', + selector: 'Message body', + text: this.text, } as const; } } } -export class ShowNoteToSelfMenuOption extends LocatorsInterface { +export class MessageInput extends LocatorsInterface { public build() { + return { + strategy: 'accessibility id', + selector: 'Message input box', + } as const; + } +} +export class MessageLengthCountdown extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private length?: string + ) { + super(device); + } + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'hide-nts-menu-option', // Yes this has the 'hide' ID + selector: 'network.loki.messenger:id/characterLimitText', + text: this.length, } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Show Note to Self', + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[@name="${this.length}"]`, + text: this.length, } as const; } } } - -export class ShowNoteToSelfConfirmButton extends LocatorsInterface { - public build() { +export class MessageLengthOkayButton extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'show-nts-confirm-button', - } as const; + return { strategy: 'id', selector: 'Okay' } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Show', - } as const; + return { strategy: 'xpath', selector: '//XCUIElementTypeButton[@name="Okay"]' } as const; } } } -export class DeleteContactMenuItem extends LocatorsInterface { + +export class MessageRequestAcceptDescription extends LocatorsInterface { public build() { + const messageRequestsAcceptDescription = englishStrippedStr( + 'messageRequestsAcceptDescription' + ).toString(); switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'delete-contact-menu-option', + selector: 'network.loki.messenger:id/sendAcceptsTextView', + text: messageRequestsAcceptDescription, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete Contact', + selector: 'Control message', + text: messageRequestsAcceptDescription, } as const; } } } -export class DeleteContactConfirmButton extends LocatorsInterface { + +export class MessageRequestPendingDescription extends LocatorsInterface { public build() { + const messageRequestPendingDescription = englishStrippedStr( + 'messageRequestPendingDescription' + ).toString(); switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'delete-contact-confirm-button', + selector: 'network.loki.messenger:id/textSendAfterApproval', + text: messageRequestPendingDescription, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete', + selector: 'Control message', + text: messageRequestPendingDescription, } as const; } } } -export class CommunityInviteConfirmButton extends LocatorsInterface { +export class NewVoiceMessageButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'qa-collapsing-footer-action_invite', - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Invite contacts button', + selector: 'New voice message', } as const; } } } -export class CommunityInvitation extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class NicknameInput extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/openGroupTitleTextView', - text: testCommunityName, + selector: 'nickname-input', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Community invitation', - text: testCommunityName, + selector: 'Username input', } as const; } } } -export class EditNicknameButton extends LocatorsInterface { +export class NotificationsModalButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': - return { - strategy: 'accessibility id', - selector: 'Edit', - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Username', + selector: 'Notifications', } as const; } } } -export class NicknameInput extends LocatorsInterface { +export class NotificationSwitch extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'nickname-input', + selector: 'com.android.settings:id/switch_text', + text: `All ${getAppDisplayName()} notifications`, } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Username input', - } as const; + throw new Error('Platform not supported'); } } } -export class SaveNicknameButton extends LocatorsInterface { +// TODO tie this to the message whose status we want to check (similar to EmojiReactsPill) +export class OutgoingMessageStatusSent extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'set-nickname-confirm-button', + selector: 'network.loki.messenger:id/messageStatusTextView', + text: 'Sent', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Save', + selector: `Message sent status: Sent`, } as const; } } @@ -551,128 +632,78 @@ export class PreferredDisplayName extends LocatorsInterface { } } -export class FirstEmojiReact extends LocatorsInterface { +export class SaveNicknameButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/reaction_1', + selector: 'set-nickname-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: '😂', + selector: 'Save', } as const; } } } -// Find the reactions pill underneath a specific message -export class EmojiReactsPill extends LocatorsInterface { - constructor( - device: DeviceWrapper, - private messageText: string - ) { - super(device); - } - - public build(): StrategyExtractionObj { +export class ScrollToBottomButton extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { - strategy: 'xpath', - selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger:id/layout_emoji_container"]`, + strategy: 'id', + selector: 'network.loki.messenger:id/scrollToBottomButton', } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="😂"]`, + strategy: 'accessibility id', + selector: 'Scroll button', } as const; } } } -export class EmojiReactsCount extends LocatorsInterface { - constructor( - device: DeviceWrapper, - private messageText: string, - private expectedCount: string = '2' - ) { - super(device); - } - +export class SendButton extends LocatorsInterface { public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'xpath', - selector: `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${this.messageText}")]]//android.widget.TextView[@resource-id="network.loki.messenger:id/reactions_pill_count"][@text="${this.expectedCount}"]`, - } as const; - case 'ios': - return { - strategy: 'xpath', - selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@label="${this.messageText}"]]//XCUIElementTypeStaticText[@value="${this.expectedCount}"]`, - } as const; - } + return { + strategy: 'accessibility id', + selector: 'Send message button', + }; } } -export class MessageLengthCountdown extends LocatorsInterface { - constructor( - device: DeviceWrapper, - private length?: string - ) { - super(device); - } - public build(): StrategyExtractionObj { +export class ShowNoteToSelfConfirmButton extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/characterLimitText', - text: this.length, + selector: 'show-nts-confirm-button', } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[@name="${this.length}"]`, - text: this.length, + strategy: 'accessibility id', + selector: 'Show', } as const; } } } -export class MessageLengthOkayButton extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { strategy: 'id', selector: 'Okay' } as const; - case 'ios': - return { strategy: 'xpath', selector: '//XCUIElementTypeButton[@name="Okay"]' } as const; - } - } -} - -export class CommunityMessageAuthor extends LocatorsInterface { - public text: string; - constructor(device: DeviceWrapper, text: string) { - super(device); - this.text = text; - } - public build(): StrategyExtractionObj { +export class ShowNoteToSelfMenuOption extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': - // Identify the profile picture of a message with a specific text return { - strategy: 'xpath', - selector: `//android.view.ViewGroup[@resource-id='network.loki.messenger:id/mainContainer'][.//android.widget.TextView[contains(@text,'${this.text}')]]//androidx.compose.ui.platform.ComposeView[@resource-id='network.loki.messenger:id/profilePictureView']`, + strategy: 'id', + selector: 'hide-nts-menu-option', // Yes this has the 'hide' ID } as const; case 'ios': - // Identify the display name of a blinded sender of a message with a specific text return { - strategy: 'xpath', - selector: `//XCUIElementTypeCell[.//XCUIElementTypeOther[@name='Message body' and contains(@label,'${this.text}')]]//XCUIElementTypeStaticText[contains(@value,'(15')]`, + strategy: 'accessibility id', + selector: 'Show Note to Self', } as const; } } @@ -695,45 +726,14 @@ export class UPMMessageButton extends LocatorsInterface { } } -export class MessageRequestPendingDescription extends LocatorsInterface { - public build() { - const messageRequestPendingDescription = englishStrippedStr( - 'messageRequestPendingDescription' - ).toString(); - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'network.loki.messenger:id/textSendAfterApproval', - text: messageRequestPendingDescription, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Control message', - text: messageRequestPendingDescription, - } as const; - } - } -} - -export class MessageRequestAcceptDescription extends LocatorsInterface { +export class VoiceMessage extends LocatorsInterface { public build() { - const messageRequestsAcceptDescription = englishStrippedStr( - 'messageRequestsAcceptDescription' - ).toString(); switch (this.platform) { case 'android': - return { - strategy: 'id', - selector: 'network.loki.messenger:id/sendAcceptsTextView', - text: messageRequestsAcceptDescription, - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Control message', - text: messageRequestsAcceptDescription, + selector: 'Voice message', } as const; } } diff --git a/run/test/specs/locators/disappearing_messages.ts b/run/test/specs/locators/disappearing_messages.ts index 48d0377e7..251b3fb77 100644 --- a/run/test/specs/locators/disappearing_messages.ts +++ b/run/test/specs/locators/disappearing_messages.ts @@ -6,81 +6,99 @@ import { StrategyExtractionObj, } from '../../../types/testing'; -export class DisappearingMessagesMenuOption extends LocatorsInterface { +export class DisableDisappearingMessages extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'disappearing-messages-menu-option', + selector: `Disable disappearing messages`, }; case 'ios': return { strategy: 'accessibility id', - selector: 'Disappearing Messages', + selector: 'Off', }; } } } -export class DisappearingMessagesSubtitle extends LocatorsInterface { +export class DisappearingMessageRadial extends LocatorsInterface { + private timer: DISAPPEARING_TIMES; + + // Receives a timer argument so that one locator can handle all DM durations + constructor(device: DeviceWrapper, timer: DISAPPEARING_TIMES) { + super(device); + this.timer = timer; + } public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Disappearing messages type and time', - }; + selector: this.timer, + } as const; case 'ios': return { strategy: 'accessibility id', - selector: `Disappearing messages type and time`, - }; + selector: `${this.timer} - Radio`, + } as const; } } } -export class DisableDisappearingMessages extends LocatorsInterface { +export class DisappearingMessagesMenuOption extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: `Disable disappearing messages`, + selector: 'disappearing-messages-menu-option', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Off', + selector: 'Disappearing Messages', }; } } } -export class SetDisappearMessagesButton extends LocatorsInterface { +export class DisappearingMessagesSubtitle extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Set button', - } as const; + selector: 'Disappearing messages type and time', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Set button', - } as const; + selector: `Disappearing messages type and time`, + }; } } } -export class SetModalButton extends LocatorsInterface { +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: 'Set', + selector: this.timerType, } as const; } } @@ -98,49 +116,31 @@ export class FollowSettingsButton extends LocatorsInterface { } } } -export class DisappearingMessageRadial extends LocatorsInterface { - private timer: DISAPPEARING_TIMES; - - // Receives a timer argument so that one locator can handle all DM durations - constructor(device: DeviceWrapper, timer: DISAPPEARING_TIMES) { - super(device); - this.timer = timer; - } +export class SetDisappearMessagesButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: this.timer, + selector: 'Set button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: `${this.timer} - Radio`, + selector: 'Set button', } as const; } } } -export class DisappearingMessagesTimerType extends LocatorsInterface { - private timerType: DisappearingOptions; - - constructor(device: DeviceWrapper, timerType: DisappearingOptions) { - super(device); - this.timerType = timerType; - } - +export class SetModalButton extends LocatorsInterface { 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, + selector: 'Set', } as const; } } diff --git a/run/test/specs/locators/external.ts b/run/test/specs/locators/external.ts index a6b7252aa..1a7ef3855 100644 --- a/run/test/specs/locators/external.ts +++ b/run/test/specs/locators/external.ts @@ -1,14 +1,5 @@ import { LocatorsInterface } from '.'; -export class PhotoLibrary extends LocatorsInterface { - public build() { - return { - strategy: 'accessibility id', - selector: 'Photos', - } as const; - } -} - export class DisguisedApp extends LocatorsInterface { public build() { return { @@ -18,20 +9,22 @@ export class DisguisedApp extends LocatorsInterface { } as const; } } -export class IOSSaveToFiles extends LocatorsInterface { + +export class iOSPhotosContinuebutton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': throw new Error('Unsupported platform'); case 'ios': return { - strategy: 'accessibility id', - selector: 'Save to Files', + strategy: 'xpath', + selector: `//XCUIElementTypeButton[@name="Continue"]`, + maxWait: 5000, } as const; } } } -export class IOSSaveButton extends LocatorsInterface { +export class IOSReplaceButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': @@ -39,12 +32,12 @@ export class IOSSaveButton extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Save', + selector: 'Replace', } as const; } } } -export class IOSReplaceButton extends LocatorsInterface { +export class IOSSaveButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': @@ -52,23 +45,30 @@ export class IOSReplaceButton extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Replace', + selector: 'Save', } as const; } } } - -export class iOSPhotosContinuebutton extends LocatorsInterface { +export class IOSSaveToFiles 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, + strategy: 'accessibility id', + selector: 'Save to Files', } as const; } } } + +export class PhotoLibrary extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'Photos', + } as const; + } +} diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index 90b072a4f..ae7e2707b 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -1,169 +1,183 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { LocatorsInterface } from './index'; -export class ModalHeading extends LocatorsInterface { +export class AccountIDDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Modal heading', + selector: 'Account ID', + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Modal heading', + selector: 'Account ID', + text: this.text, } as const; } } } -export class ModalDescription extends LocatorsInterface { +export class AllowPermissionLocator extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Modal description', + selector: 'com.android.permissioncontroller:id/permission_allow_button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Modal description', + selector: 'Allow', } as const; } } } -export class ContinueButton extends LocatorsInterface { +export class Contact extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Continue', + selector: 'pro-badge-text', + text: this.text, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Continue', + selector: 'Contact', + text: this.text, } as const; } } } -export class EnableLinkPreviewsModalButton extends LocatorsInterface { +export class ContinueButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Enable', + selector: 'Continue', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Enable', + selector: 'Continue', } as const; } } } -export class Contact extends LocatorsInterface { - public text: string | undefined; - constructor(device: DeviceWrapper, text?: string) { - super(device); - this.text = text; - } +export class CopyURLButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'pro-badge-text', - text: this.text, + selector: 'Copy URL', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Contact', - text: this.text, + selector: 'Copy URL', } as const; } } } -export class AllowPermissionLocator extends LocatorsInterface { +// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// See SES-4930 +export class CTABody extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'com.android.permissioncontroller:id/permission_allow_button', + selector: 'cta-body', } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Allow', + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, } as const; } } } -export class DenyPermissionLocator extends LocatorsInterface { +// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// See SES-4930 +export class CTAButtonNegative extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'com.android.permissioncontroller:id/permission_deny_button', + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("cta-button-negative").childSelector(new UiSelector().className("android.widget.TextView"))', // The text is not exposed to the top level selector } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Don’t Allow', + selector: 'Maybe Later', } as const; } } } -export class AccountIDDisplay extends LocatorsInterface { - public text: string | undefined; - constructor(device: DeviceWrapper, text?: string) { - super(device); - this.text = text; - } +// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// See SES-4930 +export class CTAButtonPositive extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Account ID', - text: this.text, + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("cta-button-positive").childSelector(new UiSelector().className("android.widget.TextView"))', // The text is not exposed to the top level selector } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Account ID', - text: this.text, + selector: 'Donate', } as const; } } } -export class CopyURLButton extends LocatorsInterface { +// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is not available +// See SES-4930 +export class CTAFeature extends LocatorsInterface { + private index: number; + + constructor(device: DeviceWrapper, index: number) { + super(device); + this.index = index; + } + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Copy URL', + selector: `cta-feature-${this.index}`, } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Copy URL', - } as const; + throw new Error('CTAFeature locator is not available on iOS'); } } } @@ -187,83 +201,69 @@ export class CTAHeading extends LocatorsInterface { } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 -export class CTABody extends LocatorsInterface { +export class DenyPermissionLocator extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'cta-body', + selector: 'com.android.permissioncontroller:id/permission_deny_button', } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, + strategy: 'accessibility id', + selector: 'Don’t Allow', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is not available -// See SES-4930 -export class CTAFeature extends LocatorsInterface { - private index: number; - - constructor(device: DeviceWrapper, index: number) { - super(device); - this.index = index; - } - +export class EnableLinkPreviewsModalButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: `cta-feature-${this.index}`, + selector: 'Enable', } as const; case 'ios': - throw new Error('CTAFeature locator is not available on iOS'); + return { + strategy: 'accessibility id', + selector: 'Enable', + } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 -export class CTAButtonPositive extends LocatorsInterface { +export class ModalDescription extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiSelector().resourceId("cta-button-positive").childSelector(new UiSelector().className("android.widget.TextView"))', // The text is not exposed to the top level selector + strategy: 'id', + selector: 'Modal description', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Donate', + selector: 'Modal description', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA -// See SES-4930 -export class CTAButtonNegative extends LocatorsInterface { +export class ModalHeading extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiSelector().resourceId("cta-button-negative").childSelector(new UiSelector().className("android.widget.TextView"))', // The text is not exposed to the top level selector + strategy: 'id', + selector: 'Modal heading', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Maybe Later', + selector: 'Modal heading', } as const; } } diff --git a/run/test/specs/locators/global_search.ts b/run/test/specs/locators/global_search.ts index d65f852e8..dfdfd637b 100644 --- a/run/test/specs/locators/global_search.ts +++ b/run/test/specs/locators/global_search.ts @@ -1,37 +1,37 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; -export class NoteToSelfOption extends LocatorsInterface { +export class CancelSearchButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'pro-badge-text', - text: 'Note to Self', + selector: 'network.loki.messenger:id/search_cancel', + text: 'Cancel', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Note to Self', + selector: 'Cancel', }; } } } -export class CancelSearchButton extends LocatorsInterface { +export class NoteToSelfOption extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/search_cancel', - text: 'Cancel', + selector: 'pro-badge-text', + text: 'Note to Self', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Cancel', + selector: 'Note to Self', }; } } diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index dd2dd8898..bfcd6f689 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -6,18 +6,18 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { StrategyExtractionObj } from '../../../types/testing'; import { GROUPNAME } from '../../../types/testing'; -export class GroupNameInput extends LocatorsInterface { +export class ConfirmRemovalButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Group name input', + selector: 'Remove', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Group name input', + selector: 'Remove', } as const; } } @@ -40,49 +40,71 @@ export class CreateGroupButton extends LocatorsInterface { } } -export class InviteContactConfirm extends LocatorsInterface { +export class DeleteGroupConfirm extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'qa-collapsing-footer-action_invite', + selector: 'delete-group-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Confirm invite button', + selector: 'Delete', } as const; } } } -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; +export class DeleteGroupMenuItem extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("delete-group-menu-option"))', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Leave group', // yep this is leave even for the delete option + } as const; + } } +} +export class EditGroupDescriptionInput extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': + return { + strategy: 'id', + selector: 'update-group-info-description-input', + } as const; + case 'ios': return { strategy: 'accessibility id', - selector: 'Edit', - }; - case 'ios': { - const groupName = this.groupName; - if (!groupName) { - throw new Error('groupName must be provided for iOS'); - } + selector: 'Group description text field', + } as const; + } + } +} + +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: groupName, - }; - } + selector: 'Group name text field', + } as const; } } } @@ -113,69 +135,73 @@ export class GroupDescription extends LocatorsInterface { } } -export class EditGroupNameInput extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class GroupMember extends LocatorsInterface { + public build(username?: UserNameType): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'update-group-info-name-input', + selector: 'pro-badge-text', + text: `${username}`, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Group name text field', + selector: 'Contact', + text: `${username}`, } as const; } } } -export class EditGroupDescriptionInput extends LocatorsInterface { +export class GroupNameInput extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'update-group-info-description-input', + selector: 'Group name input', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Group description text field', + selector: 'Group name input', } as const; } } } - -export class SaveGroupNameChangeButton extends LocatorsInterface { +export class InviteContactConfirm extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'update-group-info-confirm-button', + selector: 'qa-collapsing-footer-action_invite', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Save', + selector: 'Confirm invite button', } as const; } } } - -export class LeaveGroupMenuItem extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class LatestReleaseBanner extends LocatorsInterface { + public build() { switch (this.platform) { + // On Android, the text of the banner is exposed to Appium + // so it's possible to verify that the banner is visible and it has the correct text case 'android': return { strategy: 'id', - selector: 'leave-group-menu-option', + selector: 'Version warning banner', + text: englishStrippedStr('groupInviteVersion').toString(), } as const; case 'ios': + // On iOS, the text is currently not exposed to Appium return { strategy: 'accessibility id', - selector: 'Leave group', + selector: 'Version warning banner', } as const; } } @@ -196,59 +222,58 @@ export class LeaveGroupConfirm extends LocatorsInterface { } } } -export class DeleteGroupMenuItem extends LocatorsInterface { +export class LeaveGroupMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("delete-group-menu-option"))', + strategy: 'id', + selector: 'leave-group-menu-option', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Leave group', // yep this is leave even for the delete option + selector: 'Leave group', } as const; } } } -export class DeleteGroupConfirm extends LocatorsInterface { +export class ManageMembersMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'delete-group-confirm-button', - } as const; + selector: 'manage-members-menu-option', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete', - } as const; + selector: 'Manage Members', + }; } } } -export class LatestReleaseBanner extends LocatorsInterface { - public build() { + +export class MemberStatus extends LocatorsInterface { + public build(text?: string): StrategyExtractionObj { switch (this.platform) { - // On Android, the text of the banner is exposed to Appium - // so it's possible to verify that the banner is visible and it has the correct text case 'android': return { strategy: 'id', - selector: 'Version warning banner', - text: englishStrippedStr('groupInviteVersion').toString(), + selector: 'Contact status', + text, } as const; case 'ios': - // On iOS, the text is currently not exposed to Appium return { strategy: 'accessibility id', - selector: 'Version warning banner', + selector: 'Contact status', + text, } as const; } } } + export class RecreateGroupBannerAdmin extends LocatorsInterface { public build(): StrategyExtractionObj { return { @@ -286,25 +311,6 @@ export class RecreateGroupButton extends LocatorsInterface { } } -export class GroupMember extends LocatorsInterface { - public build(username?: UserNameType): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'pro-badge-text', - text: `${username}`, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Contact', - text: `${username}`, - } as const; - } - } -} - export class RemoveMemberButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -322,54 +328,48 @@ export class RemoveMemberButton extends LocatorsInterface { } } -export class ConfirmRemovalButton extends LocatorsInterface { +export class SaveGroupNameChangeButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Remove', + selector: 'update-group-info-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Remove', + selector: 'Save', } as const; } } } +export class UpdateGroupInformation extends LocatorsInterface { + private groupName?: GROUPNAME; -export class MemberStatus extends LocatorsInterface { - public build(text?: string): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Contact status', - text, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Contact status', - text, - } as const; - } + // 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; } -} -export class ManageMembersMenuItem extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'manage-members-menu-option', + strategy: 'accessibility id', + selector: 'Edit', }; - case 'ios': + case 'ios': { + const groupName = this.groupName; + if (!groupName) { + throw new Error('groupName must be provided for iOS'); + } return { strategy: 'accessibility id', - selector: 'Manage Members', + selector: groupName, }; + } } } } diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index 675968732..11059f07f 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -3,6 +3,25 @@ import type { DeviceWrapper } from '../../../types/DeviceWrapper'; import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; +export class ConversationItem extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Conversation list item', + text: this.text, + } as const; + } + } +} + export class EmptyLandingPage extends LocatorsInterface { public build() { switch (this.platform) { @@ -20,21 +39,22 @@ export class EmptyLandingPage extends LocatorsInterface { } } -export class MessageRequestsBanner extends LocatorsInterface { - public build() { +export class LongPressBlockOption extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': - case 'ios': return { strategy: 'accessibility id', - selector: 'Message requests banner', - } as const; + selector: 'Block', + }; + case 'ios': + throw new Error('Not implemented'); } } } -export class ConversationItem extends LocatorsInterface { - public text: string | undefined; +export class MessageRequestItem extends LocatorsInterface { + public text?: string | undefined; constructor(device: DeviceWrapper, text?: string) { super(device); this.text = text; @@ -45,27 +65,21 @@ export class ConversationItem extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Conversation list item', + selector: 'Message request', text: this.text, } as const; } } } -export class MessageRequestItem extends LocatorsInterface { - public text?: string | undefined; - constructor(device: DeviceWrapper, text?: string) { - super(device); - this.text = text; - } +export class MessageRequestsBanner extends LocatorsInterface { public build() { switch (this.platform) { case 'android': case 'ios': return { strategy: 'accessibility id', - selector: 'Message request', - text: this.text, + selector: 'Message requests banner', } as const; } } @@ -107,117 +121,103 @@ export class PlusButton extends LocatorsInterface { } as const; } } - -export class SearchButton extends LocatorsInterface { +export class ReviewPromptItsGreatButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: `Search icon`, + strategy: 'id', + selector: 'enjoy-session-positive-button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Search button', - }; - } - } -} -export class LongPressBlockOption extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'accessibility id', - selector: 'Block', + selector: 'enjoy-session-positive-button', }; - case 'ios': - throw new Error('Not implemented'); } } } -export class ReviewPromptItsGreatButton extends LocatorsInterface { +export class ReviewPromptNeedsWorkButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'enjoy-session-positive-button', + selector: 'enjoy-session-negative-button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'enjoy-session-positive-button', + selector: 'enjoy-session-negative-button', }; } } } -export class ReviewPromptNeedsWorkButton extends LocatorsInterface { +export class ReviewPromptNotNowButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'enjoy-session-negative-button', + selector: 'not-now-button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'enjoy-session-negative-button', + selector: 'not-now-button', }; } } } -export class ReviewPromptRateAppButton extends LocatorsInterface { +export class ReviewPromptOpenSurveyButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'rate-app-button', + selector: 'open-survey-button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'rate-app-button', + selector: 'open-survey-button', }; } } } -export class ReviewPromptNotNowButton extends LocatorsInterface { +export class ReviewPromptRateAppButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'not-now-button', + selector: 'rate-app-button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'not-now-button', + selector: 'rate-app-button', }; } } } -export class ReviewPromptOpenSurveyButton extends LocatorsInterface { +export class SearchButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'open-survey-button', + strategy: 'accessibility id', + selector: `Search icon`, }; case 'ios': return { strategy: 'accessibility id', - selector: 'open-survey-button', + selector: 'Search button', }; } } diff --git a/run/test/specs/locators/index.ts b/run/test/specs/locators/index.ts index 3dd284f78..cc075a790 100644 --- a/run/test/specs/locators/index.ts +++ b/run/test/specs/locators/index.ts @@ -29,21 +29,6 @@ export abstract class LocatorsInterface { } } -export function describeLocator(locator: StrategyExtractionObj & { text?: string }): string { - const { strategy, selector, text } = locator; - - // Trim selector if its too long, show beginning and end - const maxSelectorLength = 80; - const halfLength = Math.floor(maxSelectorLength / 2); - const trimmedSelector = - selector.length > maxSelectorLength - ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` - : selector; - - const base = `${strategy} "${trimmedSelector}"`; - return text ? `${base} and text "${text}"` : base; -} - export class ApplyChanges extends LocatorsInterface { public build() { switch (this.platform) { @@ -61,95 +46,70 @@ export class ApplyChanges extends LocatorsInterface { } } -export class ReadReceiptsButton extends LocatorsInterface { - public build() { +export class BlockedContactsSettings extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'android:id/summary', - text: 'Show read receipts for all messages you send and receive.', - } as const; + strategy: 'accessibility id', + selector: 'qa-blocked-contacts-settings-item', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Read Receipts - Switch', - } as const; + selector: 'Block contacts - Navigation', + }; } } } -export class CloseSettings extends LocatorsInterface { - public build() { +export class BlockUser extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { - case 'android': - return { - strategy: 'accessibility id', - selector: 'Close', - } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Close button', - } as const; - } - } -} - -export class UsernameDisplay extends LocatorsInterface { - public text: string | undefined; - constructor(device: DeviceWrapper, text?: string) { - super(device); - this.text = text; - } - public build() { - switch (this.platform) { + selector: 'Block', + }; case 'android': return { strategy: 'id', - selector: 'Display name', - text: this.text, - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Username', - text: this.text, - } as const; + selector: 'block-user-menu-option', + }; } } } -export class EditUsernameButton extends LocatorsInterface { - public build() { +export class BlockUserConfirmationModal extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Edit', + strategy: 'id', + selector: 'block-user-confirm-button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Username', + selector: 'Block', } as const; } } } -export class UsernameInput extends LocatorsInterface { - public build() { +export class ChangeProfilePictureButton extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'class name', - selector: 'android.widget.EditText', - } as const; + strategy: 'id', + selector: 'Image picker', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Username input', - } as const; + selector: 'Upload', + }; } } } @@ -171,295 +131,339 @@ export class ClearInputButton extends LocatorsInterface { } } -export class FirstGif extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class CloseSettings extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { - strategy: 'xpath', - selector: ANDROID_XPATHS.FIRST_GIF, - }; + strategy: 'accessibility id', + selector: 'Close', + } as const; case 'ios': return { - strategy: 'xpath', - selector: IOS_XPATHS.FIRST_GIF, - }; + strategy: 'accessibility id', + selector: 'Close button', + } as const; } } } -export class BlockUser extends LocatorsInterface { +export class CommunityInput extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Block', - }; case 'android': return { strategy: 'id', - selector: 'block-user-menu-option', + selector: 'Community input', + }; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Enter Community URL', }; } } } -export class ChangeProfilePictureButton extends LocatorsInterface { +export class DeclineMessageRequestButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Image picker', + strategy: 'accessibility id', + selector: 'Delete message request', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Upload', + selector: 'Delete message request', }; } } } -export class ImagePermissionsModalAllow extends LocatorsInterface { +export class DeleteMessageConfirmationModal extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'com.android.permissioncontroller:id/permission_allow_foreground_only_button', + selector: 'Delete', }; case 'ios': - return { strategy: 'accessibility id', selector: 'Allow Full Access' }; + return { + strategy: 'accessibility id', + selector: 'Delete', + }; } } } -export class JoinCommunityButton extends LocatorsInterface { +export class DeleteMessageForEveryone extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Join community button', + selector: 'delete-for-everyone', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Join', + selector: 'Delete for everyone', }; } } } -export class JoinCommunityModalButton extends LocatorsInterface { +export class DeleteMessageLocally extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Join', - } as const; + selector: 'delete-only-on-this-device', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Join', + selector: 'Delete for me', }; } } } -export class CommunityInput extends LocatorsInterface { +export class DeleteMessageRequestButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Community input', + selector: `android:id/title`, + text: 'Delete', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Enter Community URL', + selector: 'Delete', }; } } } -export class InviteContactsMenuItem extends LocatorsInterface { +export class DeleteMesssageRequestConfirmation extends LocatorsInterface { + public build(): StrategyExtractionObj { + return { + strategy: 'accessibility id', + selector: 'Delete', + }; + } +} + +export class DownloadMediaButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'invite-contacts-menu-option', + selector: 'Download', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Invite Contacts', + selector: 'Download', }; } } } -export class InviteAccountIDOrONS extends LocatorsInterface { +export class EditUsernameButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'invite-accountid-menu-option', + strategy: 'accessibility id', + selector: 'Edit', } as const; case 'ios': - throw new Error('Manage Members not implemented yet on iOS'); + return { + strategy: 'accessibility id', + selector: 'Username', + } as const; } } } -export class DeleteMessageLocally extends LocatorsInterface { +export class FirstGif extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'delete-only-on-this-device', + strategy: 'xpath', + selector: ANDROID_XPATHS.FIRST_GIF, }; case 'ios': return { - strategy: 'accessibility id', - selector: 'Delete for me', + strategy: 'xpath', + selector: IOS_XPATHS.FIRST_GIF, }; } } } -export class DeleteMessageForEveryone extends LocatorsInterface { +export class ImageName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { + // Dates can wildly differ between emulators but it will begin with "Photo taken on" on Android case 'android': return { - strategy: 'id', - selector: 'delete-for-everyone', + strategy: 'xpath', + selector: `//*[starts-with(@content-desc, "Photo taken on")]`, }; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Delete for everyone', - }; + throw new Error(`No such element on iOS`); } } } -export class DeleteMessageConfirmationModal extends LocatorsInterface { +export class ImagePermissionsModalAllow extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Delete', + selector: 'com.android.permissioncontroller:id/permission_allow_foreground_only_button', }; case 'ios': + return { strategy: 'accessibility id', selector: 'Allow Full Access' }; + } + } +} + +export class InviteAccountIDOrONS extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': return { - strategy: 'accessibility id', - selector: 'Delete', - }; + strategy: 'id', + selector: 'invite-accountid-menu-option', + } as const; + case 'ios': + throw new Error('Manage Members not implemented yet on iOS'); } } } -export class BlockUserConfirmationModal extends LocatorsInterface { +export class InviteContactsButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'block-user-confirm-button', - } as const; + selector: 'Invite button', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Block', - } as const; + selector: 'Invite button', + }; } } } -export class BlockedContactsSettings extends LocatorsInterface { +export class InviteContactsMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'qa-blocked-contacts-settings-item', + strategy: 'id', + selector: 'invite-contacts-menu-option', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Block contacts - Navigation', + selector: 'Invite Contacts', }; } } } -export class DeclineMessageRequestButton extends LocatorsInterface { +export class JoinCommunityButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Delete message request', + strategy: 'id', + selector: 'Join community button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete message request', + selector: 'Join', }; } } } -export class DeleteMessageRequestButton extends LocatorsInterface { +export class JoinCommunityModalButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: `android:id/title`, - text: 'Delete', - }; + selector: 'Join', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Delete', + selector: 'Join', }; } } } -export class DeleteMesssageRequestConfirmation extends LocatorsInterface { +export class LinkPreview extends LocatorsInterface { public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Delete', - }; + switch (this.platform) { + case 'android': + throw new Error(`No such element on Android`); + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Session | Send Messages, Not Metadata. | Private Messenger', + } as const; + } } } -export class DownloadMediaButton extends LocatorsInterface { +export class LinkPreviewMessage extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Download', + selector: 'network.loki.messenger:id/linkPreviewView', }; + case 'ios': + throw new Error(`No such element on iOS`); + } + } +} + +export class ReadReceiptsButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'android:id/summary', + text: 'Show read receipts for all messages you send and receive.', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Download', - }; + selector: 'Read Receipts - Switch', + } as const; } } } @@ -480,45 +484,58 @@ export class ShareExtensionIcon extends LocatorsInterface { } } } -export class LinkPreview extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class UsernameDisplay extends LocatorsInterface { + public text: string | undefined; + constructor(device: DeviceWrapper, text?: string) { + super(device); + this.text = text; + } + public build() { switch (this.platform) { case 'android': - throw new Error(`No such element on Android`); + return { + strategy: 'id', + selector: 'Display name', + text: this.text, + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Session | Send Messages, Not Metadata. | Private Messenger', + selector: 'Username', + text: this.text, } as const; } } } -export class LinkPreviewMessage extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class UsernameInput extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'network.loki.messenger:id/linkPreviewView', - }; + strategy: 'class name', + selector: 'android.widget.EditText', + } as const; case 'ios': - throw new Error(`No such element on iOS`); + return { + strategy: 'accessibility id', + selector: 'Username input', + } as const; } } } -export class ImageName extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - // Dates can wildly differ between emulators but it will begin with "Photo taken on" on Android - case 'android': - return { - strategy: 'xpath', - selector: `//*[starts-with(@content-desc, "Photo taken on")]`, - }; - case 'ios': - throw new Error(`No such element on iOS`); - } - } +export function describeLocator(locator: StrategyExtractionObj & { text?: string }): string { + const { strategy, selector, text } = locator; + + // Trim selector if its too long, show beginning and end + const maxSelectorLength = 80; + const halfLength = Math.floor(maxSelectorLength / 2); + const trimmedSelector = + selector.length > maxSelectorLength + ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` + : selector; + + const base = `${strategy} "${trimmedSelector}"`; + return text ? `${base} and text "${text}"` : base; } diff --git a/run/test/specs/locators/network_page.ts b/run/test/specs/locators/network_page.ts index 6b577f89e..b5673aa2a 100644 --- a/run/test/specs/locators/network_page.ts +++ b/run/test/specs/locators/network_page.ts @@ -2,159 +2,159 @@ import { LocatorsInterface } from '.'; import { englishStrippedStr } from '../../../localizer/englishStrippedStr'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; -export class SessionNetworkMenuItem extends LocatorsInterface { +export class LastUpdatedTimeStamp extends LocatorsInterface { + private expectedText: string; + + constructor(device: DeviceWrapper, relative_time: string) { + super(device); + this.expectedText = englishStrippedStr('updated').withArgs({ relative_time }).toString(); + } public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'session-network-menu-item', + selector: 'Last updated timestamp', + text: this.expectedText, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Session Network', + selector: 'Last updated timestamp', + text: this.expectedText, } as const; } } } -export class SessionNetworkLearnMoreNetwork extends LocatorsInterface { +export class MarketCapAmount extends LocatorsInterface { + private expectedText: string; + + constructor(device: DeviceWrapper, amount: number) { + super(device); + // Round to whole number, then format with commas and USD suffix + const rounded = Math.round(amount); + this.expectedText = `$${rounded.toLocaleString('en-US')} USD`; + } + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Learn more link', + selector: 'Market cap amount', + text: this.expectedText, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Learn more link', + selector: 'Market cap amount', + text: this.expectedText, } as const; } } } -export class SessionNetworkLearnMoreStaking extends LocatorsInterface { +export class OpenLinkButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Learn about staking link', + selector: 'Open', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Learn about staking link', + selector: 'Open', } as const; } } } -export class LastUpdatedTimeStamp extends LocatorsInterface { +export class SESHPrice extends LocatorsInterface { private expectedText: string; - constructor(device: DeviceWrapper, relative_time: string) { + constructor(device: DeviceWrapper, priceValue: number) { super(device); - this.expectedText = englishStrippedStr('updated').withArgs({ relative_time }).toString(); + this.expectedText = `$${priceValue.toFixed(2)} USD`; } + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Last updated timestamp', + selector: 'SESH price', text: this.expectedText, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Last updated timestamp', + selector: 'SENT price', text: this.expectedText, } as const; } } } -export class OpenLinkButton extends LocatorsInterface { +export class SessionNetworkLearnMoreNetwork extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Open', + selector: 'Learn more link', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Open', + selector: 'Learn more link', } as const; } } } -export class SESHPrice extends LocatorsInterface { - private expectedText: string; - - constructor(device: DeviceWrapper, priceValue: number) { - super(device); - this.expectedText = `$${priceValue.toFixed(2)} USD`; - } - +export class SessionNetworkLearnMoreStaking extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'SESH price', - text: this.expectedText, + selector: 'Learn about staking link', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'SENT price', - text: this.expectedText, + selector: 'Learn about staking link', } as const; } } } -export class StakingRewardPoolAmount extends LocatorsInterface { - private expectedText: string; - - constructor(device: DeviceWrapper, amount: number) { - super(device); - // Format with commas and SESH suffix - this.expectedText = `${amount.toLocaleString('en-US')} SESH`; - } - +export class SessionNetworkMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Staking reward pool amount', - text: this.expectedText, + selector: 'session-network-menu-item', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Staking reward pool amount', - text: this.expectedText, + selector: 'Session Network', } as const; } } } -export class MarketCapAmount extends LocatorsInterface { +export class StakingRewardPoolAmount extends LocatorsInterface { private expectedText: string; constructor(device: DeviceWrapper, amount: number) { super(device); - // Round to whole number, then format with commas and USD suffix - const rounded = Math.round(amount); - this.expectedText = `$${rounded.toLocaleString('en-US')} USD`; + // Format with commas and SESH suffix + this.expectedText = `${amount.toLocaleString('en-US')} SESH`; } public build() { @@ -162,13 +162,13 @@ export class MarketCapAmount extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'Market cap amount', + selector: 'Staking reward pool amount', text: this.expectedText, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Market cap amount', + selector: 'Staking reward pool amount', text: this.expectedText, } as const; } diff --git a/run/test/specs/locators/onboarding.ts b/run/test/specs/locators/onboarding.ts index b2f0dee47..5512025c8 100644 --- a/run/test/specs/locators/onboarding.ts +++ b/run/test/specs/locators/onboarding.ts @@ -1,22 +1,18 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; -// SHARED LOCATORS - -export class ErrorMessage extends LocatorsInterface { +export class AccountRestoreButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'error-message', - maxWait: 5000, + selector: 'Restore your session button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Error message', - maxWait: 5000, + selector: 'Restore your session button', } as const; } } @@ -39,16 +35,6 @@ export class BackButton extends LocatorsInterface { } } -export class WarningModalQuitButton extends LocatorsInterface { - public build(): StrategyExtractionObj { - return { - strategy: 'id', - selector: 'Quit', - } as const; - } -} - -// SPLASH SCREEN export class CreateAccountButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -66,94 +52,92 @@ export class CreateAccountButton extends LocatorsInterface { } } -export class AccountRestoreButton extends LocatorsInterface { +export class DisplayNameInput extends LocatorsInterface { public build() { 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', + selector: 'Enter display name', + } as const; + case 'android': + return { + strategy: 'id', + selector: 'Enter display name', } as const; } } } -export class SplashScreenLinks extends LocatorsInterface { +export class ErrorMessage extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Open URL', + selector: 'error-message', + maxWait: 5_000, } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Open URL', + selector: 'Error message', + maxWait: 5_000, } as const; } } } -export class TermsOfServiceButton extends LocatorsInterface { + +export class FastModeRadio extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Terms of service button', + selector: 'Fast mode notifications button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Terms of Service', + selector: 'Fast mode notifications button', } as const; } } } - -export class PrivacyPolicyButton extends LocatorsInterface { +export class LoadingAnimation extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Privacy policy button', + selector: 'Loading animation', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Privacy Policy', + selector: 'Loading animation', } as const; } } } -// CREATE ACCOUNT FLOW - -export class DisplayNameInput extends LocatorsInterface { +export class PrivacyPolicyButton extends LocatorsInterface { public build() { switch (this.platform) { - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Enter display name', - } as const; case 'android': return { strategy: 'id', - selector: 'Enter display name', + selector: 'Privacy policy button', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Privacy Policy', } as const; } } } -// LOAD ACCOUNT FLOW - export class SeedPhraseInput extends LocatorsInterface { public build() { switch (this.platform) { @@ -171,55 +155,62 @@ export class SeedPhraseInput extends LocatorsInterface { } } -// MESSAGE NOTIFICATIONS - -export class FastModeRadio extends LocatorsInterface { +export class SlowModeRadio extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Fast mode notifications button', + selector: 'Slow mode notifications button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Fast mode notifications button', + selector: 'Slow mode notifications button', } as const; } } } -export class SlowModeRadio extends LocatorsInterface { +export class SplashScreenLinks extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Slow mode notifications button', + selector: 'Open URL', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Slow mode notifications button', + selector: 'Open URL', } as const; } } } -export class LoadingAnimation extends LocatorsInterface { +export class TermsOfServiceButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Loading animation', + selector: 'Terms of service button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Loading animation', + selector: 'Terms of Service', } as const; } } } + +export class WarningModalQuitButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + return { + strategy: 'id', + selector: 'Quit', + } as const; + } +} diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index 742940bf0..f4264c988 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -1,363 +1,363 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; -export class HideRecoveryPasswordButton extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class AppDisguiseMeetingIcon extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Hide recovery password button', + selector: 'MeetingSE option', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Hide recovery password button', + selector: 'Meetings option', } as const; } } } -export class YesButton extends LocatorsInterface { +export class AppDisguisePage extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Yes', + strategy: 'class name', + selector: 'android.widget.ScrollView', } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Yes', + strategy: 'class name', + selector: 'XCUIElementTypeTable', } as const; } } } -export class UserSettings extends LocatorsInterface { - public build() { - return { - strategy: 'accessibility id', - selector: 'User settings', - } as const; - } -} - -export class UserAvatar extends LocatorsInterface { +export class AppearanceMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'User settings', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Appearance"))', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'User settings', + selector: 'Appearance', } as const; } } } -export class RecoveryPasswordMenuItem extends LocatorsInterface { +export class ClassicLightThemeOption extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Recovery password menu item', + selector: 'network.loki.messenger:id/theme_option_classic_light', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Recovery password menu item', + selector: 'Classic Light', } as const; } } } -export class RevealRecoveryPhraseButton extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class CloseAppButton extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Reveal recovery phrase button', - }; + strategy: 'class name', + selector: 'android.widget.TextView', + text: 'Close App', + } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Continue', - }; + throw new Error('Modal not implemented for iOS'); } } } -export class RecoveryPhraseContainer extends LocatorsInterface { +export class CommunityMessageRequestSwitch extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Recovery password container', + strategy: '-android uiautomator', + selector: 'new UiSelector().text("Community Message Requests")', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Recovery password container', + selector: 'Community Message Requests', } as const; } } } -export class CommunityMessageRequestSwitch extends LocatorsInterface { +export class ConversationsMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: '-android uiautomator', - selector: 'new UiSelector().text("Community Message Requests")', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Conversations"))', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Community Message Requests', + selector: 'Conversations', } as const; } } } -export class SaveProfilePictureButton extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class DonationsMenuItem extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Save', + selector: 'donate-menu-item', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Save', + selector: 'Donate', } as const; } } } -export class SaveNameChangeButton extends LocatorsInterface { + +export class HideRecoveryPasswordButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'update-username-confirm-button', + selector: 'Hide recovery password button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Save', + selector: 'Hide recovery password button', } as const; } } } - -export class PrivacyMenuItem extends LocatorsInterface { +export class NotificationsMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Privacy', + selector: 'Notifications', } as const; case 'ios': - return { strategy: 'id', selector: 'Privacy' } as const; + return { + strategy: 'accessibility id', + selector: 'Notifications', + } as const; } } } -export class ConversationsMenuItem extends LocatorsInterface { +export class PathMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Conversations"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Conversations', + selector: 'Path', } as const; } } } -export class NotificationsMenuItem extends LocatorsInterface { +export class PrivacyMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Notifications', + selector: 'Privacy', } as const; case 'ios': - return { - strategy: 'accessibility id', - selector: 'Notifications', - } as const; + return { strategy: 'id', selector: 'Privacy' } as const; } } } -export class AppearanceMenuItem extends LocatorsInterface { +export class RecoveryPasswordMenuItem extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Appearance"))', + strategy: 'id', + selector: 'Recovery password menu item', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Appearance', + selector: 'Recovery password menu item', } as const; } } } -export class ClassicLightThemeOption extends LocatorsInterface { - public build() { +export class RecoveryPhraseContainer extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'network.loki.messenger:id/theme_option_classic_light', + selector: 'Recovery password container', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Classic Light', + selector: 'Recovery password container', } as const; } } } -export class SelectAppIcon extends LocatorsInterface { - public build() { +export class RevealRecoveryPhraseButton extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Select app icon"))', - } as const; + strategy: 'id', + selector: 'Reveal recovery phrase button', + }; case 'ios': return { strategy: 'accessibility id', - selector: 'Select alternate app icon', - } as const; + selector: 'Continue', + }; } } } -export class AppDisguisePage extends LocatorsInterface { - public build() { + +export class SaveNameChangeButton extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'class name', - selector: 'android.widget.ScrollView', + strategy: 'id', + selector: 'update-username-confirm-button', } as const; case 'ios': return { - strategy: 'class name', - selector: 'XCUIElementTypeTable', + strategy: 'accessibility id', + selector: 'Save', } as const; } } } -export class AppDisguiseMeetingIcon extends LocatorsInterface { - public build() { +export class SaveProfilePictureButton extends LocatorsInterface { + public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'MeetingSE option', + selector: 'Save', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Meetings option', + selector: 'Save', } as const; } } } - -export class CloseAppButton extends LocatorsInterface { +export class SelectAppIcon extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: 'class name', - selector: 'android.widget.TextView', - text: 'Close App', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Select app icon"))', } as const; case 'ios': - throw new Error('Modal not implemented for iOS'); + return { + strategy: 'accessibility id', + selector: 'Select alternate app icon', + } as const; } } } -export class DonationsMenuItem extends LocatorsInterface { + +export class UserAvatar extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'donate-menu-item', + selector: 'User settings', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Donate', + selector: 'User settings', } as const; } } } +export class UserSettings extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'User settings', + } as const; + } +} -export class PathMenuItem extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class VersionNumber extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: '-android uiautomator', selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("path-menu-item"))', + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith("Version"))', } as const; case 'ios': return { - strategy: 'accessibility id', - selector: 'Path', + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[contains(@name, "Version")]`, } as const; } } } -export class VersionNumber extends LocatorsInterface { +export class YesButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { - strategy: '-android uiautomator', - selector: - 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith("Version"))', + strategy: 'id', + selector: 'Yes', } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[contains(@name, "Version")]`, + strategy: 'accessibility id', + selector: 'Yes', } as const; } } diff --git a/run/test/specs/locators/start_conversation.ts b/run/test/specs/locators/start_conversation.ts index 92042db94..5115c0309 100644 --- a/run/test/specs/locators/start_conversation.ts +++ b/run/test/specs/locators/start_conversation.ts @@ -1,18 +1,35 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; -export class NewMessageOption extends LocatorsInterface { +export class CloseButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'accessibility id', + selector: 'Close', + }; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'X', + }; + } + } +} + +export class CopyButton extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'New direct message', + selector: 'Copy button', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'New direct message', + selector: 'Copy button', } as const; } } @@ -35,19 +52,19 @@ export class CreateGroupOption extends LocatorsInterface { } } -export class JoinCommunityOption extends LocatorsInterface { +export class EnterAccountID extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Join community button', - }; + selector: 'Session id input box', + } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Join Community', - }; + selector: 'Session id input box', + } as const; } } } @@ -69,40 +86,39 @@ export class InviteAFriendOption extends LocatorsInterface { } } -export class CloseButton extends LocatorsInterface { +export class JoinCommunityOption extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'Close', + strategy: 'id', + selector: 'Join community button', }; case 'ios': return { strategy: 'accessibility id', - selector: 'X', + selector: 'Join Community', }; } } } -export class CopyButton extends LocatorsInterface { +export class NewMessageOption extends LocatorsInterface { public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'Copy button', + selector: 'New direct message', } as const; case 'ios': return { strategy: 'accessibility id', - selector: 'Copy button', + selector: 'New direct message', } as const; } } } - export class NextButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -119,25 +135,6 @@ export class NextButton extends LocatorsInterface { } } } -// NEW MESSAGE SECTION -export class EnterAccountID extends LocatorsInterface { - public build(): StrategyExtractionObj { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'Session id input box', - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Session id input box', - } as const; - } - } -} - -// INVITE A FRIEND SECTION export class ShareButton extends LocatorsInterface { public build() { From 74508f1e50b8b1b58c981423851cff56f9f31399 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 8 Jan 2026 16:44:24 +1100 Subject: [PATCH 008/184] feat: add promote test --- run/test/specs/group_tests_promote.spec.ts | 98 ++++++++++++++++++++++ run/types/testing.ts | 5 ++ 2 files changed, 103 insertions(+) create mode 100644 run/test/specs/group_tests_promote.spec.ts diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts new file mode 100644 index 000000000..fb59c33bd --- /dev/null +++ b/run/test/specs/group_tests_promote.spec.ts @@ -0,0 +1,98 @@ +import type { TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { androidIt } from '../../types/sessionIt'; +import { DISAPPEARING_TIMES } from '../../types/testing'; +import { ConversationSettings } from './locators/conversation'; +import { Contact } from './locators/global'; +import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; +import { setDisappearingMessage } from './utils/set_disappearing_messages'; + +androidIt({ + title: 'Promote to admin', + risk: 'medium', + testCb: promoteToAdmin, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: 'Verifies that a group member can be promoted to Admin.', +}); + +const time = DISAPPEARING_TIMES.ONE_MINUTE; +const timerType = 'Disappear after send option'; + +// TODO tidy this up with locators +async function promoteToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Test group'; + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { bob }, + } = await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + // Navigate to Promote Members screen + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'manage-admins-menu-option', + }); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'promote-members-menu-option', + }); + await alice1.clickOnElementAll(new Contact(alice1, 'Bob')); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'qa-collapsing-footer-action_promote', + }); + await alice1.checkModalStrings( + englishStrippedStr('promote').toString(), + englishStrippedStr('adminPromoteDescription').withArgs({ name: bob.userName }).toString() + ); + await alice1.waitForTextElementToBePresent({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('promoteAdminsWarning').toString()}")`, + }); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'Promote', + }); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'Confirm', + }); + await alice1.navigateBack(); + await alice1.navigateBack(); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('adminPromotedToAdmin').withArgs({ name: bob.userName }).toString(), + 30_000 + ) + ) + ); + await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); + // Check to see if Bob has admin powers by setting disappearing messages + await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) + .toString() + ) + ) + ); + await bob1.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: 'sent' }) + .toString() + ); + await closeApp(alice1, bob1, charlie1); +} diff --git a/run/types/testing.ts b/run/types/testing.ts index c71c5f7f3..ca1542b92 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -453,6 +453,7 @@ export type Id = | 'com.android.settings:id/switch_text' | 'com.google.android.apps.photos:id/sign_in_button' | 'Community input' + | 'Confirm' | 'Confirm invite button' | 'Contact' | 'Contact status' @@ -513,6 +514,7 @@ export type Id = | 'leave-group-menu-option' | 'Leave' | 'Loading animation' + | 'manage-admins-menu-option' | 'manage-members-menu-option' | 'Market cap amount' | 'MeetingSE option' @@ -562,7 +564,10 @@ export type Id = | 'Privacy' | 'Privacy policy button' | 'pro-badge-text' + | 'promote-members-menu-option' + | 'Promote' | 'qa-collapsing-footer-action_invite' + | 'qa-collapsing-footer-action_promote' | 'qa-collapsing-footer-action_remove' | 'Quit' | 'rate-app-button' From fd982b08d81a1a3e593c678e83be39909e0e302f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 9 Jan 2026 16:44:56 +1100 Subject: [PATCH 009/184] chore: use correct qa-tags --- run/test/specs/group_tests_add_accountid.spec.ts | 6 ++++-- run/test/specs/group_tests_add_contact.spec.ts | 10 ++++++---- .../specs/group_tests_add_contact_nohistory.spec.ts | 10 ++++++---- run/test/specs/group_tests_kick_member.spec.ts | 1 + .../specs/group_tests_kick_member_messages.spec.ts | 11 ++++++----- run/test/specs/group_tests_promote.spec.ts | 2 +- run/types/testing.ts | 4 ++++ 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 7901d7c89..e7eda8ca0 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -17,7 +17,7 @@ import { closeApp, SupportedPlatformsType } from './utils/open_app'; androidIt({ title: 'Invite Account ID to group', risk: 'high', - testCb: addContactToGroup, + testCb: addAccountIDToGroup, countOfDevicesNeeded: 4, allureSuites: { parent: 'Groups', @@ -26,7 +26,9 @@ androidIt({ allureDescription: 'Verifies that inviting a non-contact Account ID (without chat history) works as expected.', }); -async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + +// TODO proper locator classes, test.steps +async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Group to test adding contact'; const { devices: { alice1, bob1, charlie1, unknown1 }, diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 29195462f..cf874db1e 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -17,7 +17,7 @@ import { closeApp, SupportedPlatformsType } from './utils/open_app'; androidIt({ title: 'Invite contact to group with chat history', risk: 'high', - testCb: addContactToGroup, + testCb: addContactToGroupHistory, countOfDevicesNeeded: 4, allureSuites: { parent: 'Groups', @@ -26,7 +26,9 @@ androidIt({ allureDescription: 'Verifies that inviting a contact to a group with message history works as expected.', }); -async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + +// TODO proper locator classes, test.steps +async function addContactToGroupHistory(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Group to test adding contact'; const { devices: { alice1, bob1, charlie1, unknown1 }, @@ -65,8 +67,8 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); await alice1.clickOnElementAll({ - strategy: '-android uiautomator', - selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareMessageHistoryDays').toString()}")`, + strategy: 'id', + selector: 'share-message-history-option', }); await alice1.clickOnElementAll({ strategy: 'id', diff --git a/run/test/specs/group_tests_add_contact_nohistory.spec.ts b/run/test/specs/group_tests_add_contact_nohistory.spec.ts index 5ec74938a..75e1f126e 100644 --- a/run/test/specs/group_tests_add_contact_nohistory.spec.ts +++ b/run/test/specs/group_tests_add_contact_nohistory.spec.ts @@ -17,7 +17,7 @@ import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ title: 'Invite contact to group without chat history', risk: 'high', - testCb: addContactToGroup, + testCb: addContactToGroupNoHistory, countOfDevicesNeeded: 4, allureSuites: { parent: 'Groups', @@ -26,7 +26,9 @@ bothPlatformsIt({ allureDescription: 'Verifies that inviting a contact (Android: without chat history) works as expected.', }); -async function addContactToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + +// TODO proper locator classes, test.steps +async function addContactToGroupNoHistory(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Group to test adding contact'; const { devices: { alice1, bob1, charlie1, unknown1 }, @@ -66,8 +68,8 @@ async function addContactToGroup(platform: SupportedPlatformsType, testInfo: Tes await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); if (platform === 'android') { await alice1.clickOnElementAll({ - strategy: '-android uiautomator', - selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareNewMessagesOnly').toString()}")`, + strategy: 'id', + selector: 'share-new-messages-option', }); await alice1.clickOnElementAll({ strategy: 'id', diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index 3c3a1555a..b5edaa3a0 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -26,6 +26,7 @@ bothPlatformsIt({ 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group.', }); +// TODO proper locator classes, test.steps async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Kick member'; diff --git a/run/test/specs/group_tests_kick_member_messages.spec.ts b/run/test/specs/group_tests_kick_member_messages.spec.ts index 12d1d4b5d..51335abc2 100644 --- a/run/test/specs/group_tests_kick_member_messages.spec.ts +++ b/run/test/specs/group_tests_kick_member_messages.spec.ts @@ -22,17 +22,18 @@ import { SupportedPlatformsType } from './utils/open_app'; androidIt({ title: 'Kick and remove messages', risk: 'medium', - testCb: kickMember, + testCb: kickMemberDeleteMsg, countOfDevicesNeeded: 3, allureSuites: { parent: 'Groups', suite: 'Edit Group', }, allureDescription: - 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group.', + 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group (with their messages deleted).', }); -async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) { +// TODO proper locator classes, test.steps +async function kickMemberDeleteMsg(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Kick member'; const { @@ -59,8 +60,8 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) .toString() ); await alice1.clickOnElementAll({ - strategy: '-android uiautomator', - selector: `new UiSelector().text("${englishStrippedStr('removeMemberMessages').withArgs({ count: 1 }).toString()}")`, + strategy: 'id', + selector: 'remove-member-messages-option', }); await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); // The Group Member element sometimes disappears slowly, sometimes quickly. diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index fb59c33bd..841ef6829 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -24,7 +24,7 @@ androidIt({ const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; -// TODO tidy this up with locators +// TODO proper locator classes, test.steps async function promoteToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Test group'; const { diff --git a/run/types/testing.ts b/run/types/testing.ts index ca1542b92..d49dc4988 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -574,6 +574,8 @@ export type Id = | 'Recovery password container' | 'Recovery password menu item' | 'Recovery phrase input' + | 'remove-member-messages-option' + | 'remove-member-option' | 'Remove' | 'Remove contact button' | 'Restore your session button' @@ -586,6 +588,8 @@ export type Id = | 'Session id input box' | 'set-nickname-confirm-button' | 'Set button' + | 'share-message-history-option' + | 'share-new-messages-option' | 'Share button' | 'show-nts-confirm-button' | 'Show' From 6ff9141390519b8b663ce170c70a4628c70d35e7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 12 Jan 2026 16:59:43 +1100 Subject: [PATCH 010/184] chore: begin implementing new groups locators --- .../specs/group_tests_add_accountid.spec.ts | 29 +++++-- .../specs/group_tests_add_contact.spec.ts | 11 +-- .../group_tests_add_contact_nohistory.spec.ts | 11 +-- .../group_tests_kick_member_messages.spec.ts | 6 +- run/test/specs/locators/groups.ts | 82 +++++++++++-------- 5 files changed, 84 insertions(+), 55 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index e7eda8ca0..e878c155b 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -5,8 +5,8 @@ import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteAccountIDOrONS } from './locators'; import { ConversationSettings, MessageBody } from './locators/conversation'; -import { ManageMembersMenuItem } from './locators/groups'; -import { ConversationItem, MessageRequestsBanner } from './locators/home'; +import { ManageMembersMenuItem, ShareNewMessagesRadial } from './locators/groups'; +import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { EnterAccountID, NextButton } from './locators/start_conversation'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; import { sleepFor } from './utils'; @@ -32,13 +32,14 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T const testGroupName = 'Group to test adding contact'; const { devices: { alice1, bob1, charlie1, unknown1 }, - prebuilt: { alice, group }, + prebuilt: { alice }, } = await open_Alice1_Bob1_Charlie1_Unknown1({ platform, groupName: testGroupName, focusGroupConvo: true, testInfo: testInfo, }); + const aliceTruncatedPubkey = truncatePubkey(alice.sessionId, platform); const historicMsg = `Hello from ${alice.userName}`; await alice1.sendMessage(historicMsg); await Promise.all( @@ -48,6 +49,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T ); const userD = await newUser(unknown1, USERNAME.DRACULA); const userDTruncatedPubkey = truncatePubkey(userD.accountID, platform); + const userDMsg = `Hello from ${userD.userName}`; // Click more options await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Select edit group @@ -57,10 +59,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T await alice1.clickOnElementAll(new InviteAccountIDOrONS(alice1)); await alice1.inputText(userD.accountID, new EnterAccountID(alice1)); await alice1.clickOnElementAll(new NextButton(alice1)); - await alice1.clickOnElementAll({ - strategy: '-android uiautomator', - selector: `new UiSelector().text("${englishStrippedStr('membersInviteShareNewMessagesOnly').toString()}")`, - }); + await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); await alice1.clickOnElementAll({ strategy: 'id', selector: 'Send Invite', @@ -79,8 +78,20 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T ) ); await unknown1.clickOnElementAll(new MessageRequestsBanner(unknown1)); - await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); - await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.clickOnElementAll(new MessageRequestItem(unknown1)); + await unknown1.waitForControlMessageToBePresent( + englishStrippedStr('messageRequestGroupInvite') + .withArgs({ name: aliceTruncatedPubkey, group_name: testGroupName }) + .toString() + ); + await unknown1.clickOnByAccessibilityID('Accept message request'); await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); + await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.sendMessage(userDMsg); + await Promise.all( + [alice1, bob1, charlie1, unknown1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, userDMsg)) + ) + ); await closeApp(alice1, bob1, charlie1, unknown1); } diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index cf874db1e..a84069983 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -6,7 +6,11 @@ import { USERNAME } from '../../types/testing'; import { InviteContactsMenuItem } from './locators'; import { ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; -import { InviteContactConfirm, ManageMembersMenuItem } from './locators/groups'; +import { + InviteContactConfirm, + ManageMembersMenuItem, + ShareMessageHistoryRadial, +} from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; import { sleepFor } from './utils'; @@ -66,10 +70,7 @@ async function addContactToGroupHistory(platform: SupportedPlatformsType, testIn text: USERNAME.DRACULA, }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'share-message-history-option', - }); + await alice1.clickOnElementAll(new ShareMessageHistoryRadial(alice1)); await alice1.clickOnElementAll({ strategy: 'id', selector: 'Send Invite', diff --git a/run/test/specs/group_tests_add_contact_nohistory.spec.ts b/run/test/specs/group_tests_add_contact_nohistory.spec.ts index 75e1f126e..0b16efd6f 100644 --- a/run/test/specs/group_tests_add_contact_nohistory.spec.ts +++ b/run/test/specs/group_tests_add_contact_nohistory.spec.ts @@ -6,7 +6,11 @@ import { USERNAME } from '../../types/testing'; import { InviteContactsMenuItem } from './locators'; import { ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; -import { InviteContactConfirm, ManageMembersMenuItem } from './locators/groups'; +import { + InviteContactConfirm, + ManageMembersMenuItem, + ShareNewMessagesRadial, +} from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; import { sleepFor } from './utils'; @@ -67,10 +71,7 @@ async function addContactToGroupNoHistory(platform: SupportedPlatformsType, test }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); if (platform === 'android') { - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'share-new-messages-option', - }); + await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); await alice1.clickOnElementAll({ strategy: 'id', selector: 'Send Invite', diff --git a/run/test/specs/group_tests_kick_member_messages.spec.ts b/run/test/specs/group_tests_kick_member_messages.spec.ts index 51335abc2..1f3488941 100644 --- a/run/test/specs/group_tests_kick_member_messages.spec.ts +++ b/run/test/specs/group_tests_kick_member_messages.spec.ts @@ -14,6 +14,7 @@ import { GroupMember, ManageMembersMenuItem, RemoveMemberButton, + RemoveMemberMessagesRadial, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { SupportedPlatformsType } from './utils/open_app'; @@ -59,10 +60,7 @@ async function kickMemberDeleteMsg(platform: SupportedPlatformsType, testInfo: T .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) .toString() ); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'remove-member-messages-option', - }); + await alice1.clickOnElementAll(new RemoveMemberMessagesRadial(alice1)); await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); // The Group Member element sometimes disappears slowly, sometimes quickly. // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index bfcd6f689..998789a6d 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -274,60 +274,49 @@ export class MemberStatus extends LocatorsInterface { } } -export class RecreateGroupBannerAdmin extends LocatorsInterface { - public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Legacy group banner', - text: englishStrippedStr('legacyGroupAfterDeprecationAdmin').toString(), - } as const; - } -} - -export class RecreateGroupBannerMember extends LocatorsInterface { - public build(): StrategyExtractionObj { - return { - strategy: 'accessibility id', - selector: 'Legacy group banner', - text: englishStrippedStr('legacyGroupAfterDeprecationMember').toString(), - } as const; - } -} - -export class RecreateGroupButton extends LocatorsInterface { +export class RemoveMemberButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { - case 'ios': + case 'android': return { - strategy: 'accessibility id', - selector: 'Legacy Groups Recreate Button', + strategy: 'id', + selector: 'qa-collapsing-footer-action_remove', } as const; - case 'android': + case 'ios': return { strategy: 'accessibility id', - selector: 'Accept message request', + selector: 'Remove contact button', } as const; } } } -export class RemoveMemberButton extends LocatorsInterface { - public build(): StrategyExtractionObj { +export class RemoveMemberMessagesRadial extends LocatorsInterface { + public build() { switch (this.platform) { case 'android': return { strategy: 'id', - selector: 'qa-collapsing-footer-action_remove', + selector: 'remove-member-messages-option', } as const; case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} +export class RemoveMemberRadial extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': return { - strategy: 'accessibility id', - selector: 'Remove contact button', + strategy: 'id', + selector: 'remove-member-option', } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); } } } - export class SaveGroupNameChangeButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -344,6 +333,35 @@ export class SaveGroupNameChangeButton extends LocatorsInterface { } } } + +export class ShareMessageHistoryRadial extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'share-message-history-option', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + +export class ShareNewMessagesRadial extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'share-new-messages-option', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + export class UpdateGroupInformation extends LocatorsInterface { private groupName?: GROUPNAME; From 4adc83041db614236e469268ed1230b382a05205 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 12 Jan 2026 16:59:58 +1100 Subject: [PATCH 011/184] chore: weed out message request item locators --- run/test/specs/community_requests_on.spec.ts | 4 ++-- run/test/specs/message_requests_accept.spec.ts | 4 ++-- run/test/specs/message_requests_accept_text_reply.spec.ts | 4 ++-- run/test/specs/message_requests_block.spec.ts | 4 ++-- run/test/specs/message_requests_decline.spec.ts | 2 +- run/test/specs/message_requests_delete.spec.ts | 2 +- run/test/specs/user_actions_create_contact.spec.ts | 4 ++-- run/test/specs/user_actions_delete_contact_ucs.spec.ts | 4 ++-- run/test/specs/utils/create_contact.ts | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index 1a7a53b89..5eac3b98a 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -13,7 +13,7 @@ import { MessageRequestPendingDescription, UPMMessageButton, } from './locators/conversation'; -import { MessageRequestsBanner } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { CommunityMessageRequestSwitch, PrivacyMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; @@ -76,7 +76,7 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo await test.step(`${bob.userName} accepts message request from ${alice.userName}`, async () => { await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Bob clicks on request conversation item - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); await device2.waitForTextElementToBePresent( new ConversationHeaderName(device2, alice.userName) ); diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index cdce72623..e74d78c82 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationItem, MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppThreeDevices, SupportedPlatformsType } from './utils/open_app'; @@ -29,7 +29,7 @@ async function acceptRequest(platform: SupportedPlatformsType, testInfo: TestInf // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Bob clicks on request conversation item - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); // Bob clicks accept button on device 2 (original device) await device2.clickOnByAccessibilityID('Accept message request'); // Check control message for message request acceptance 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 525912dd6..9dca293e7 100644 --- a/run/test/specs/message_requests_accept_text_reply.spec.ts +++ b/run/test/specs/message_requests_accept_text_reply.spec.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { MessageInput, OutgoingMessageStatusSent, SendButton } from './locators/conversation'; -import { PlusButton } from './locators/home'; +import { MessageRequestItem, PlusButton } from './locators/home'; import { MessageRequestsBanner } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; @@ -59,7 +59,7 @@ async function acceptRequestWithText(platform: SupportedPlatformsType, testInfo: // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Bob clicks on request conversation item - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); // Check control message warning of sending message request reply // "messageRequestsAcceptDescription": "Sending a message to this user will automatically accept their message request and reveal your Account ID." const messageRequestsAcceptDescription = englishStrippedStr( diff --git a/run/test/specs/message_requests_block.spec.ts b/run/test/specs/message_requests_block.spec.ts index 8a9399f89..cb4e5c3a2 100644 --- a/run/test/specs/message_requests_block.spec.ts +++ b/run/test/specs/message_requests_block.spec.ts @@ -5,7 +5,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { type AccessibilityId, USERNAME } from '../../types/testing'; import { BlockedContactsSettings } from './locators'; import { Contact } from './locators/global'; -import { MessageRequestsBanner, PlusButton } from './locators/home'; +import { MessageRequestItem, MessageRequestsBanner, PlusButton } from './locators/home'; import { ConversationsMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; @@ -35,7 +35,7 @@ async function blockedRequest(platform: SupportedPlatformsType, testInfo: TestIn // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Bob clicks on request conversation item - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); // Bob clicks on block option await device2.clickOnByAccessibilityID('Block message request'); // Confirm block on android diff --git a/run/test/specs/message_requests_decline.spec.ts b/run/test/specs/message_requests_decline.spec.ts index 442a6032d..bc84b6312 100644 --- a/run/test/specs/message_requests_decline.spec.ts +++ b/run/test/specs/message_requests_decline.spec.ts @@ -29,7 +29,7 @@ async function declineRequest(platform: SupportedPlatformsType, testInfo: TestIn // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Bob clicks on request conversation item - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); // Check message request appears on linked device (device 3) await device3.clickOnElementAll(new MessageRequestsBanner(device3)); await device3.waitForTextElementToBePresent(new MessageRequestItem(device3)); diff --git a/run/test/specs/message_requests_delete.spec.ts b/run/test/specs/message_requests_delete.spec.ts index a67a40d35..b084c532c 100644 --- a/run/test/specs/message_requests_delete.spec.ts +++ b/run/test/specs/message_requests_delete.spec.ts @@ -27,7 +27,7 @@ async function deleteRequest(platform: SupportedPlatformsType, testInfo: TestInf // Bob clicks on message request banner await device2.clickOnElementAll(new MessageRequestsBanner(device2)); // Swipe left on ios - await device2.onIOS().swipeLeftAny('Message request'); + await device2.onIOS().swipeLeftAny(new MessageRequestItem(device2).build().selector); await device2.onAndroid().longPress(new MessageRequestItem(device2)); await device2.clickOnElementAll(new DeleteMessageRequestButton(device2)); await device2.checkModalStrings( diff --git a/run/test/specs/user_actions_create_contact.spec.ts b/run/test/specs/user_actions_create_contact.spec.ts index 3dd56e1d1..9b6993df8 100644 --- a/run/test/specs/user_actions_create_contact.spec.ts +++ b/run/test/specs/user_actions_create_contact.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationItem, MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { newUser } from './utils/create_account'; import { retryMsgSentForBanner } from './utils/create_contact'; import { linkedDevice } from './utils/link_device'; @@ -29,7 +29,7 @@ async function createContact(platform: SupportedPlatformsType, testInfo: TestInf await runOnlyOnIOS(platform, () => retryMsgSentForBanner(platform, device1, device2, 30000)); // this runOnlyOnIOS is needed await device2.clickOnElementAll(new MessageRequestsBanner(device2)); - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); await device2.clickOnByAccessibilityID('Accept message request'); // Type into message input box diff --git a/run/test/specs/user_actions_delete_contact_ucs.spec.ts b/run/test/specs/user_actions_delete_contact_ucs.spec.ts index b6e34e506..a046564e4 100644 --- a/run/test/specs/user_actions_delete_contact_ucs.spec.ts +++ b/run/test/specs/user_actions_delete_contact_ucs.spec.ts @@ -10,7 +10,7 @@ import { DeleteContactMenuItem, MessageBody, } from './locators/conversation'; -import { ConversationItem, MessageRequestsBanner } from './locators/home'; +import { ConversationItem, MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { open_Alice2_Bob1_friends } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; @@ -78,7 +78,7 @@ async function deleteContactCS(platform: SupportedPlatformsType, testInfo: TestI await Promise.all( [alice1, alice2].map(async device => { await device.clickOnElementAll(new MessageRequestsBanner(device)); - await device.clickOnByAccessibilityID('Message request'); + await device.clickOnElementAll(new MessageRequestItem(device)); await device.waitForTextElementToBePresent(new MessageBody(device, newMessage)); }) ); diff --git a/run/test/specs/utils/create_contact.ts b/run/test/specs/utils/create_contact.ts index 0316eacbf..79770df01 100644 --- a/run/test/specs/utils/create_contact.ts +++ b/run/test/specs/utils/create_contact.ts @@ -2,7 +2,7 @@ import { runOnlyOnIOS, sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; import { MessageBody } from '../locators/conversation'; -import { MessageRequestsBanner } from '../locators/home'; +import { MessageRequestItem, MessageRequestsBanner } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; export const newContact = async ( @@ -18,7 +18,7 @@ export const newContact = async ( await runOnlyOnIOS(platform, () => retryMsgSentForBanner(platform, device1, device2, 30000)); // this runOnlyOnIOS is needed await device2.clickOnElementAll(new MessageRequestsBanner(device2)); - await device2.clickOnByAccessibilityID('Message request'); + await device2.clickOnElementAll(new MessageRequestItem(device2)); await device2.onAndroid().clickOnByAccessibilityID('Accept message request'); // Type into message input box const replyMessage = `${receiver.userName} to ${sender.userName}`; From 0aedace583f385d13024c40f911ff34e72930a62 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 12 Jan 2026 17:09:01 +1100 Subject: [PATCH 012/184] feat: add accept message request method --- run/test/specs/group_tests_add_accountid.spec.ts | 8 ++++++-- run/test/specs/locators/conversation.ts | 10 +++++++++- run/test/specs/message_requests_accept.spec.ts | 10 ++-------- run/test/specs/user_actions_create_contact.spec.ts | 6 ++---- run/test/specs/utils/create_contact.ts | 6 ++---- run/types/DeviceWrapper.ts | 14 +++++++++++++- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index e878c155b..445d30387 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -4,7 +4,11 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteAccountIDOrONS } from './locators'; -import { ConversationSettings, MessageBody } from './locators/conversation'; +import { + AcceptMessageRequestButton, + ConversationSettings, + MessageBody, +} from './locators/conversation'; import { ManageMembersMenuItem, ShareNewMessagesRadial } from './locators/groups'; import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { EnterAccountID, NextButton } from './locators/start_conversation'; @@ -84,7 +88,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T .withArgs({ name: aliceTruncatedPubkey, group_name: testGroupName }) .toString() ); - await unknown1.clickOnByAccessibilityID('Accept message request'); + await unknown1.clickOnElementAll(new AcceptMessageRequestButton(unknown1)); await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); await unknown1.sendMessage(userDMsg); diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 8018d14ea..82ae025fe 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -6,6 +6,15 @@ import { StrategyExtractionObj } from '../../../types/testing'; import { getAppDisplayName } from '../utils/devnet'; import { LocatorsInterface } from './index'; +export class AcceptMessageRequestButton extends LocatorsInterface { + public build() { + return { + strategy: 'accessibility id', + selector: 'Accept message request', + } as const; + } +} + export class AttachmentsButton extends LocatorsInterface { public build() { return { @@ -14,7 +23,6 @@ export class AttachmentsButton extends LocatorsInterface { } as const; } } - export class BlockedBanner extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/message_requests_accept.spec.ts b/run/test/specs/message_requests_accept.spec.ts index e74d78c82..b0dd4a451 100644 --- a/run/test/specs/message_requests_accept.spec.ts +++ b/run/test/specs/message_requests_accept.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationItem, MessageRequestItem, MessageRequestsBanner } from './locators/home'; +import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { linkedDevice } from './utils/link_device'; import { closeApp, openAppThreeDevices, SupportedPlatformsType } from './utils/open_app'; @@ -25,13 +25,7 @@ async function acceptRequest(platform: SupportedPlatformsType, testInfo: TestInf // Send message from Alice to Bob await device1.sendNewMessage(bob, `${alice.userName} to ${bob.userName}`); - // Wait for banner to appear - // Bob clicks on message request banner - await device2.clickOnElementAll(new MessageRequestsBanner(device2)); - // Bob clicks on request conversation item - await device2.clickOnElementAll(new MessageRequestItem(device2)); - // Bob clicks accept button on device 2 (original device) - await device2.clickOnByAccessibilityID('Accept message request'); + await device2.acceptMessageRequestWithButton(); // Check control message for message request acceptance // "messageRequestsAccepted": "Your message request has been accepted.", const messageRequestsAccepted = englishStrippedStr('messageRequestsAccepted').toString(); diff --git a/run/test/specs/user_actions_create_contact.spec.ts b/run/test/specs/user_actions_create_contact.spec.ts index 9b6993df8..c2c505452 100644 --- a/run/test/specs/user_actions_create_contact.spec.ts +++ b/run/test/specs/user_actions_create_contact.spec.ts @@ -2,7 +2,7 @@ import type { TestInfo } from '@playwright/test'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationItem, MessageRequestItem, MessageRequestsBanner } from './locators/home'; +import { ConversationItem } from './locators/home'; import { newUser } from './utils/create_account'; import { retryMsgSentForBanner } from './utils/create_contact'; import { linkedDevice } from './utils/link_device'; @@ -28,9 +28,7 @@ async function createContact(platform: SupportedPlatformsType, testInfo: TestInf await sleepFor(100); await runOnlyOnIOS(platform, () => retryMsgSentForBanner(platform, device1, device2, 30000)); // this runOnlyOnIOS is needed - await device2.clickOnElementAll(new MessageRequestsBanner(device2)); - await device2.clickOnElementAll(new MessageRequestItem(device2)); - await device2.clickOnByAccessibilityID('Accept message request'); + await device2.acceptMessageRequestWithButton(); // Type into message input box await device2.sendMessage(`Reply-message-${Bob.userName}-to-${Alice.userName}`); diff --git a/run/test/specs/utils/create_contact.ts b/run/test/specs/utils/create_contact.ts index 79770df01..4bfabc2f8 100644 --- a/run/test/specs/utils/create_contact.ts +++ b/run/test/specs/utils/create_contact.ts @@ -2,7 +2,7 @@ import { runOnlyOnIOS, sleepFor } from '.'; import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; import { MessageBody } from '../locators/conversation'; -import { MessageRequestItem, MessageRequestsBanner } from '../locators/home'; +import { MessageRequestsBanner } from '../locators/home'; import { SupportedPlatformsType } from './open_app'; export const newContact = async ( @@ -17,9 +17,7 @@ export const newContact = async ( await sleepFor(100); await runOnlyOnIOS(platform, () => retryMsgSentForBanner(platform, device1, device2, 30000)); // this runOnlyOnIOS is needed - await device2.clickOnElementAll(new MessageRequestsBanner(device2)); - await device2.clickOnElementAll(new MessageRequestItem(device2)); - await device2.onAndroid().clickOnByAccessibilityID('Accept message request'); + await device2.acceptMessageRequestWithButton(); // Type into message input box const replyMessage = `${receiver.userName} to ${sender.userName}`; await device2.sendMessage(replyMessage); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f10ef2777..11ed10159 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -30,6 +30,7 @@ import { } from '../constants/testfiles'; import { englishStrippedStr } from '../localizer/englishStrippedStr'; import { + AcceptMessageRequestButton, AttachmentsButton, DocumentsFolderButton, GIFButton, @@ -51,7 +52,12 @@ import { ModalDescription, ModalHeading, } from '../test/specs/locators/global'; -import { ConversationItem, PlusButton } from '../test/specs/locators/home'; +import { + ConversationItem, + MessageRequestItem, + MessageRequestsBanner, + PlusButton, +} from '../test/specs/locators/home'; import { LoadingAnimation } from '../test/specs/locators/onboarding'; import { PrivacyMenuItem, @@ -1767,6 +1773,12 @@ export class DeviceWrapper { return message; } + public async acceptMessageRequestWithButton() { + await this.clickOnElementAll(new MessageRequestsBanner(this)); + await this.clickOnElementAll(new MessageRequestItem(this)); + await this.onAndroid().clickOnElementAll(new AcceptMessageRequestButton(this)); + } + public async sendMessageTo(sender: User, receiver: Group | User) { const message = `${sender.userName} to ${receiver.userName}`; await this.clickOnElementAll(new ConversationItem(this, receiver.userName)); From 7fcd8b594fd6f008e02b6e3ca0b4d1c5082574a5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 16 Jan 2026 11:07:53 +1100 Subject: [PATCH 013/184] chore: update android notifications screenshot --- run/screenshots/android/settings_notifications.png | 4 ++-- run/test/specs/utils/verify_screenshots.ts | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/run/screenshots/android/settings_notifications.png b/run/screenshots/android/settings_notifications.png index 0cdc813c4..0d9ee91e3 100644 --- a/run/screenshots/android/settings_notifications.png +++ b/run/screenshots/android/settings_notifications.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:846ea6ce60f9014e0fa36823e176da62f6f27fc63026ea1291f66b69cebf0730 -size 142963 +oid sha256:e26cf860dbfb83a797c7465c826098601d275d15a4c69ee2dfe083d790661bfb +size 154470 diff --git a/run/test/specs/utils/verify_screenshots.ts b/run/test/specs/utils/verify_screenshots.ts index 9ef8aa769..1ba3bd67f 100644 --- a/run/test/specs/utils/verify_screenshots.ts +++ b/run/test/specs/utils/verify_screenshots.ts @@ -167,10 +167,7 @@ function ensureBaseline(actualBuffer: Buffer, baselinePath: string): void { // fs.mkdirSync(path.dirname(baselinePath), { recursive: true }); // fs.writeFileSync(baselinePath, actualBuffer); - throw new Error( - `No baseline image found at: ${baselinePath}. \n - A new screenshot has been saved at: ${tempPath}` - ); + throw new Error(`No baseline image found. A new screenshot has been saved at: ${baselinePath}`); } } From 0dec36ee3c5a1da8a310ff561db8ef26009c0025 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 16 Jan 2026 14:35:20 +1100 Subject: [PATCH 014/184] fix: remove animation env var from ios caps --- run/test/specs/utils/capabilities_ios.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index a4f8ba65a..ad45fa016 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -34,7 +34,6 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { env: { debugDisappearingMessageDurations: 'true', communityPollLimit: '3', - animationsEnabled: 'true', // App crashes on some un-animated transitions, see SES-5064 }, }, } as AppiumXCUITestCapabilities; From 4e2bb7ba7c42a73323de73cdef58f9550a7f7743 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 16 Jan 2026 16:33:00 +1100 Subject: [PATCH 015/184] feat: add test for background perms modal android --- run/test/specs/app_disguise_set.spec.ts | 19 +++---- run/test/specs/locators/home.ts | 30 ++++++++++- run/test/specs/slow_mode_background.spec.ts | 59 +++++++++++++++++++++ run/test/specs/utils/create_account.ts | 53 +++++++++++++----- run/test/specs/utils/link_device.ts | 4 +- run/test/specs/utils/open_app.ts | 18 ++++++- run/test/specs/utils/permissions.ts | 35 ++++++++++-- run/test/specs/utils/restore_account.ts | 6 +-- run/types/allure.ts | 2 +- run/types/testing.ts | 3 ++ 10 files changed, 191 insertions(+), 38 deletions(-) create mode 100644 run/test/specs/slow_mode_background.spec.ts diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index bb47ca2f3..29774bc16 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -13,13 +13,13 @@ import { UserSettings, } from './locators/settings'; import { sleepFor } from './utils'; -import { getAdbFullPath } from './utils/binaries'; -import { androidAppPackage } from './utils/capabilities_android'; -import { iOSBundleId } from './utils/capabilities_ios'; import { newUser } from './utils/create_account'; -import { openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { + openAppOnPlatformSingleDevice, + SupportedPlatformsType, + uninstallApp, +} from './utils/open_app'; import { closeApp } from './utils/open_app'; -import { runScriptAndLog } from './utils/utilities'; bothPlatformsItSeparate({ title: 'App disguise set icon', @@ -67,7 +67,7 @@ async function appDisguiseSetIconIOS(platform: SupportedPlatformsType, testInfo: // The disguised app must be uninstalled otherwise every following test will fail await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); - await runScriptAndLog(`xcrun simctl uninstall ${device.udid} ${iOSBundleId}`, true); + await uninstallApp(device, platform); }); } }); @@ -87,7 +87,7 @@ async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testI await device.clickOnElementAll(new SelectAppIcon(device)); try { await device.clickOnElementAll(new AppDisguiseMeetingIcon(device)); - await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disgusie'), async () => { + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('app disguise'), async () => { await device.checkModalStrings( englishStrippedStr('appIconAndNameChange').toString(), englishStrippedStr('appIconAndNameChangeConfirmation').toString() @@ -104,10 +104,7 @@ async function appDisguiseSetIconAndroid(platform: SupportedPlatformsType, testI // The disguised app must be uninstalled otherwise every following test will fail await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); - await runScriptAndLog( - `${getAdbFullPath()} -s ${device.udid} uninstall ${androidAppPackage}`, - true - ); + await uninstallApp(device, platform); }); } }); diff --git a/run/test/specs/locators/home.ts b/run/test/specs/locators/home.ts index 11059f07f..784d6b68b 100644 --- a/run/test/specs/locators/home.ts +++ b/run/test/specs/locators/home.ts @@ -3,6 +3,34 @@ import type { DeviceWrapper } from '../../../types/DeviceWrapper'; import { StrategyExtractionObj } from '../../../types/testing'; import { LocatorsInterface } from './index'; +export class BackgroundPermsAllowButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'whitelist-confirm-button', + } as const; + case 'ios': + throw new Error('Not implemented'); + } + } +} + +export class BackgroundPermsCancelButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'whitelist-cancel-button', + } as const; + case 'ios': + throw new Error('Not implemented'); + } + } +} + export class ConversationItem extends LocatorsInterface { public text: string | undefined; constructor(device: DeviceWrapper, text?: string) { @@ -112,7 +140,6 @@ export class MessageSnippet extends LocatorsInterface { } } } - export class PlusButton extends LocatorsInterface { public build() { return { @@ -121,6 +148,7 @@ export class PlusButton extends LocatorsInterface { } as const; } } + export class ReviewPromptItsGreatButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/specs/slow_mode_background.spec.ts b/run/test/specs/slow_mode_background.spec.ts new file mode 100644 index 000000000..ea7a8a8a1 --- /dev/null +++ b/run/test/specs/slow_mode_background.spec.ts @@ -0,0 +1,59 @@ +import test, { TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { BackgroundPermsAllowButton } from './locators/home'; +import { newUser } from './utils/create_account'; +import { + closeApp, + openAppOnPlatformSingleDevice, + SupportedPlatformsType, + uninstallApp, +} from './utils/open_app'; + +androidIt({ + title: 'Slow mode background perms modal', + risk: 'medium', + testCb: slowModeBackgroundModal, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Notifications', + }, + allureDescription: + 'Verifies the slow mode background permissions modal appears, accepting it shows the system dialog.', +}); + +async function slowModeBackgroundModal(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { + saveUserData: false, + fastMode: false, + }); + return { device }; + }); + try { + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Background Permissions'), async () => { + await device.checkModalStrings( + englishStrippedStr('runSessionBackground').toString(), + englishStrippedStr('runSessionBackgroundDescription').toString() + ); + await device.clickOnElementAll(new BackgroundPermsAllowButton(device)); + await device.clickOnElementAll({ + strategy: 'id', + selector: 'android:id/button1', + text: 'Allow', + }); + }); + // The test ends here since there is no good way to verify that the specific toggle is ON. + } finally { + // App must be uninstalled to prevent state pollution (background permission is tied to app install) + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + await uninstallApp(device, platform); + }); + } +} diff --git a/run/test/specs/utils/create_account.ts b/run/test/specs/utils/create_account.ts index 180acdbc1..d27cdaebd 100644 --- a/run/test/specs/utils/create_account.ts +++ b/run/test/specs/utils/create_account.ts @@ -4,18 +4,39 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { User } from '../../../types/testing'; import { CloseSettings } from '../locators'; import { AccountIDDisplay, ContinueButton } from '../locators/global'; -import { CreateAccountButton, DisplayNameInput, FastModeRadio } from '../locators/onboarding'; +import { + CreateAccountButton, + DisplayNameInput, + FastModeRadio, + SlowModeRadio, +} from '../locators/onboarding'; import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; import { UserSettings } from '../locators/settings'; import { CopyButton } from '../locators/start_conversation'; -import { handlePermissions } from './permissions'; +import { handleBackgroundPermissions, handleNotificationPermissions } from './permissions'; export type BaseSetupOptions = { allowNotificationPermissions?: boolean; }; +/** + * Setup options for account creation specifically + * + * By default, new accounts will: + * - set fast mode + * - deny notification permissions + * + * If fast mode is `false` and allowBackgroundPermissions is not explicitly set, + * the test will have to handle the background permissions modal on Android. + * Tests that *do* grant background permissions must clean up with a try/finally uninstall + * to avoid state pollution in following tests. + * + * Note that this is all theoretically possible in restore account as well, we just don't bother to do it. + */ export type NewUserSetupOptions = BaseSetupOptions & { saveUserData?: boolean; + fastMode?: boolean; + allowBackgroundPermissions?: boolean; }; export async function newUser( @@ -23,39 +44,43 @@ export async function newUser( userName: UserNameType, options?: NewUserSetupOptions ): Promise { - const { saveUserData = true, allowNotificationPermissions = false } = options || {}; + const { + saveUserData = true, + allowNotificationPermissions = false, + allowBackgroundPermissions, + fastMode = true, + } = options || {}; device.setDeviceIdentity(`${userName.toLowerCase()}1`); - // Click create session ID await device.clickOnElementAll(new CreateAccountButton(device)); - // Input username await device.inputText(userName, new DisplayNameInput(device)); - // Click continue await device.clickOnElementAll(new ContinueButton(device)); // Choose message notification options (Fast mode by default) - // TODO: Add option to choose slow mode and handle bg perms on Android (SES-4975) - await device.clickOnElementAll(new FastModeRadio(device)); - // Select Continue to save notification settings + if (fastMode) { + await device.clickOnElementAll(new FastModeRadio(device)); + } else await device.clickOnElementAll(new SlowModeRadio(device)); await device.clickOnElementAll(new ContinueButton(device)); // Handle permissions based on the flag - await handlePermissions(device, allowNotificationPermissions); + await handleNotificationPermissions(device, allowNotificationPermissions); + if (!fastMode) { + await handleBackgroundPermissions(device, allowBackgroundPermissions); + } // Some tests don't need to save the Account ID and Recovery Password if (!saveUserData) { return { userName, accountID: 'not_needed', recoveryPhrase: 'not_needed' }; } - // Click on 'continue' button to open recovery phrase modal + // Open recovery phrase modal and save recovery phrase await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); - //Save recovery password const recoveryPhraseContainer = await device.clickOnElementAll( new RecoveryPhraseContainer(device) ); await device.onAndroid().clickOnElementAll(new CopyButton(device)); - // Save recovery phrase as variable const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); device.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); - // Exit Modal await device.navigateBack(false); + + // Get Account ID from User Settings await device.clickOnElementAll(new UserSettings(device)); const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); diff --git a/run/test/specs/utils/link_device.ts b/run/test/specs/utils/link_device.ts index d9f755a74..87c23d30e 100644 --- a/run/test/specs/utils/link_device.ts +++ b/run/test/specs/utils/link_device.ts @@ -12,7 +12,7 @@ import { } from '../locators/onboarding'; import { newUser } from './create_account'; import { BaseSetupOptions } from './create_account'; -import { handlePermissions } from './permissions'; +import { handleNotificationPermissions } from './permissions'; export const linkedDevice = async ( device1: DeviceWrapper, @@ -49,7 +49,7 @@ export const linkedDevice = async ( device2.info('Display name found: Loading account'); } // Wait for permissions modal to pop up - await handlePermissions(device2, allowNotificationPermissions); + await handleNotificationPermissions(device2, allowNotificationPermissions); // Check that button was clicked await device2.waitForTextElementToBePresent(new PlusButton(device2)); diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index 5099c18c3..263616be8 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -13,8 +13,13 @@ import { getEmulatorFullPath, getSdkManagerFullPath, } from './binaries'; -import { getAndroidCapabilities, getAndroidUdid } from './capabilities_android'; -import { CapabilitiesIndexType, capabilityIsValid, getIosCapabilities } from './capabilities_ios'; +import { androidAppPackage, getAndroidCapabilities, getAndroidUdid } from './capabilities_android'; +import { + CapabilitiesIndexType, + capabilityIsValid, + getIosCapabilities, + iOSBundleId, +} from './capabilities_ios'; import { cleanPermissions } from './permissions'; import { registerDevicesForTest } from './screenshot_helper'; import { sleepFor } from './sleep_for'; @@ -347,3 +352,12 @@ export const closeApp = async (...devices: Array) => { console.info('sessions closed'); }; + +export const uninstallApp = async (device: DeviceWrapper, platform: SupportedPlatformsType) => { + const command = + platform === 'android' + ? `${getAdbFullPath()} -s ${device.udid} uninstall ${androidAppPackage}` + : `xcrun simctl uninstall ${device.udid} ${iOSBundleId}`; + + await runScriptAndLog(command, true); +}; diff --git a/run/test/specs/utils/permissions.ts b/run/test/specs/utils/permissions.ts index 03338821d..22e95e1c7 100644 --- a/run/test/specs/utils/permissions.ts +++ b/run/test/specs/utils/permissions.ts @@ -4,6 +4,7 @@ import { XCUITestDriver, XCUITestDriverOpts } from 'appium-xcuitest-driver/build import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { AllowPermissionLocator, DenyPermissionLocator } from '../locators/global'; +import { BackgroundPermsAllowButton, BackgroundPermsCancelButton } from '../locators/home'; import { runScriptAndLog } from './utilities'; export const cleanPermissions = async ( @@ -61,13 +62,39 @@ export const cleanPermissions = async ( 'Failed to open the iOS app and find the Create account button after multiple retries.' ); }; -export const handlePermissions = async ( +export const handleNotificationPermissions = async ( device: DeviceWrapper, - allowPermissions: boolean = false + allowNotificationPermissions: boolean = false ) => { - const permissionLocator = allowPermissions + const notificationPermsLocator = allowNotificationPermissions ? new AllowPermissionLocator(device) : new DenyPermissionLocator(device); - await device.processPermissions(permissionLocator); + await device.processPermissions(notificationPermsLocator); +}; + +/** + * Handles the background permissions modal that appears in slow mode on Android. + * + * @param allowBackgroundPermissions + * - `undefined`: Modal is not handled - test must interact with it manually + * - `true`: Auto-allow background permissions + * - `false`: Auto-deny background permissions + */ +export const handleBackgroundPermissions = async ( + device: DeviceWrapper, + allowBackgroundPermissions?: boolean +) => { + if (allowBackgroundPermissions == undefined) return; + + if (allowBackgroundPermissions) { + await device.clickOnElementAll(new BackgroundPermsAllowButton(device)); + await device.clickOnElementAll({ + strategy: 'id', + selector: 'android:id/button1', + text: 'Allow', + }); + } else { + await device.clickOnElementAll(new BackgroundPermsCancelButton(device)); + } }; diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index 289473bd5..cb496910d 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -10,7 +10,7 @@ import { SeedPhraseInput, } from '../locators/onboarding'; import { BaseSetupOptions } from './create_account'; -import { handlePermissions } from './permissions'; +import { handleNotificationPermissions } from './permissions'; export const restoreAccount = async ( device: DeviceWrapper, @@ -41,7 +41,7 @@ export const restoreAccount = async ( device.info('Display name found: Loading account'); } // Wait for permissions modal to pop up - await handlePermissions(device, allowNotificationPermissions); + await handleNotificationPermissions(device, allowNotificationPermissions); // Check that we're on the home screen await device.waitForTextElementToBePresent(new PlusButton(device)); }; @@ -79,7 +79,7 @@ export const restoreAccountNoFallback = async ( // Wait for permissions modal to pop up await sleepFor(500); - await handlePermissions(device, allowNotificationPermissions); + await handleNotificationPermissions(device, allowNotificationPermissions); await sleepFor(1000); // Check that we're on the home screen await device.waitForTextElementToBePresent(new PlusButton(device)); diff --git a/run/types/allure.ts b/run/types/allure.ts index 2f9ff1430..62d0d00ec 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -31,7 +31,7 @@ export type AllureSuiteConfig = parent: 'Sending Messages'; suite: 'Emoji reacts' | 'Mentions' | 'Message types' | 'Performance' | 'Rules'; } - | { parent: 'Settings'; suite: 'App Disguise' | 'Community Message Requests' } + | { parent: 'Settings'; suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' } | { parent: 'User Actions'; suite: diff --git a/run/types/testing.ts b/run/types/testing.ts index e117bd39c..7d8f931ee 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -432,6 +432,7 @@ export type Id = | 'android:id/aerr_close' | 'android:id/aerr_wait' | 'android:id/alertTitle' + | 'android:id/button1' | 'android:id/content_preview_text' | 'android:id/summary' | 'android:id/title' @@ -592,6 +593,8 @@ export type Id = | 'update-username-confirm-button' | 'User settings' | 'Version warning banner' + | 'whitelist-cancel-button' + | 'whitelist-confirm-button' | 'Yes' | `All ${AppName} notifications` | `cta-feature-${number}` From 9a36ea285a4d0967a125201a442031d6498cc402 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 19 Jan 2026 16:01:56 +1100 Subject: [PATCH 016/184] chore: use correct locator --- run/test/specs/locators/groups.ts | 2 +- run/types/DeviceWrapper.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index d6875d160..032bba16f 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -68,7 +68,7 @@ export class DeleteGroupMenuItem extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Leave group', // yep this is leave even for the delete option + selector: 'Delete group', } as const; } } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f10ef2777..b3d6f5dc0 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -346,6 +346,7 @@ export class DeviceWrapper { { from: 'Message sent status: Sent', to: 'Message sent status: Sending' }, { from: 'Done', to: 'Donate' }, { from: 'New conversation button', to: 'conversation-options-avatar' }, + { from: 'Leave group', to: 'Delete group' }, ]; // System locators such as 'network.loki.messenger:id' can cause false positives with too high similarity scores From 1f42fe47551e412c05d397e3bdb09db920273ebd Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 20 Jan 2026 15:26:29 +1100 Subject: [PATCH 017/184] chore: continue adding locator classes and test steps --- .../specs/group_tests_add_accountid.spec.ts | 11 +- .../specs/group_tests_add_contact.spec.ts | 6 +- .../group_tests_add_contact_nohistory.spec.ts | 6 +- .../specs/group_tests_kick_member.spec.ts | 20 +-- .../group_tests_kick_member_messages.spec.ts | 4 +- run/test/specs/group_tests_promote.spec.ts | 136 ++++++++++-------- run/test/specs/locators/groups.ts | 87 ++++++++++- 7 files changed, 181 insertions(+), 89 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 445d30387..55bef893c 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -9,7 +9,11 @@ import { ConversationSettings, MessageBody, } from './locators/conversation'; -import { ManageMembersMenuItem, ShareNewMessagesRadial } from './locators/groups'; +import { + InviteContactSendInviteButton, + ManageMembersMenuItem, + ShareNewMessagesRadial, +} from './locators/groups'; import { MessageRequestItem, MessageRequestsBanner } from './locators/home'; import { EnterAccountID, NextButton } from './locators/start_conversation'; import { open_Alice1_Bob1_Charlie1_Unknown1 } from './state_builder'; @@ -64,10 +68,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T await alice1.inputText(userD.accountID, new EnterAccountID(alice1)); await alice1.clickOnElementAll(new NextButton(alice1)); await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'Send Invite', - }); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); // Leave Manage Members await alice1.navigateBack(); // Leave Conversation Settings diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index a84069983..2abc00a49 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -8,6 +8,7 @@ import { ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; import { InviteContactConfirm, + InviteContactSendInviteButton, ManageMembersMenuItem, ShareMessageHistoryRadial, } from './locators/groups'; @@ -71,10 +72,7 @@ async function addContactToGroupHistory(platform: SupportedPlatformsType, testIn }); await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); await alice1.clickOnElementAll(new ShareMessageHistoryRadial(alice1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'Send Invite', - }); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); // Leave Manage Members await alice1.navigateBack(); // Leave Conversation Settings diff --git a/run/test/specs/group_tests_add_contact_nohistory.spec.ts b/run/test/specs/group_tests_add_contact_nohistory.spec.ts index 0b16efd6f..1287c56c4 100644 --- a/run/test/specs/group_tests_add_contact_nohistory.spec.ts +++ b/run/test/specs/group_tests_add_contact_nohistory.spec.ts @@ -8,6 +8,7 @@ import { ConversationSettings, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; import { InviteContactConfirm, + InviteContactSendInviteButton, ManageMembersMenuItem, ShareNewMessagesRadial, } from './locators/groups'; @@ -72,10 +73,7 @@ async function addContactToGroupNoHistory(platform: SupportedPlatformsType, test await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); if (platform === 'android') { await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'Send Invite', - }); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); } // Leave Manage Members await alice1.navigateBack(); diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index b5edaa3a0..6697cc01f 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -3,7 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { ConversationSettings, MessageInput } from './locators/conversation'; +import { ConversationSettings, EmptyConversation, MessageInput } from './locators/conversation'; import { ConfirmRemovalButton, GroupMember, @@ -65,15 +65,15 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() ), ]); - await bob1.onAndroid().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Empty list', - text: englishStrippedStr('groupRemovedYou').withArgs({ group_name: testGroupName }).toString(), - }); - await bob1.onIOS().waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Empty list', - }); + await bob1 + .onAndroid() + .waitForTextElementToBePresent({ + ...new EmptyConversation(bob1).build(), + text: englishStrippedStr('groupRemovedYou') + .withArgs({ group_name: testGroupName }) + .toString(), + }); + await bob1.onIOS().waitForTextElementToBePresent(new EmptyConversation(bob1)); // Message input should not be present after being kicked await bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1000 }); } diff --git a/run/test/specs/group_tests_kick_member_messages.spec.ts b/run/test/specs/group_tests_kick_member_messages.spec.ts index 1f3488941..efcdb34da 100644 --- a/run/test/specs/group_tests_kick_member_messages.spec.ts +++ b/run/test/specs/group_tests_kick_member_messages.spec.ts @@ -6,6 +6,7 @@ import { USERNAME } from '../../types/testing'; import { ConversationSettings, DeletedMessage, + EmptyConversation, MessageBody, MessageInput, } from './locators/conversation'; @@ -85,8 +86,7 @@ async function kickMemberDeleteMsg(platform: SupportedPlatformsType, testInfo: T ); await Promise.all([ bob1.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Empty list', + ...new EmptyConversation(bob1).build(), text: englishStrippedStr('groupRemovedYou') .withArgs({ group_name: testGroupName }) .toString(), diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index 841ef6829..88be01275 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -1,10 +1,18 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; +import { + ConfirmPromotionModalButton, + ManageAdminsMenuItem, + PromoteMemberFooterButton, + PromoteMemberModalConfirm, + PromoteMembersMenuItem, +} from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; @@ -21,78 +29,82 @@ androidIt({ allureDescription: 'Verifies that a group member can be promoted to Admin.', }); +// The newly promoted admin will set disappearing messages to verify they have admin powers const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; -// TODO proper locator classes, test.steps async function promoteToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Test group'; const { devices: { alice1, bob1, charlie1 }, - prebuilt: { bob }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); }); - // Navigate to Promote Members screen - await alice1.clickOnElementAll(new ConversationSettings(alice1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'manage-admins-menu-option', - }); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'promote-members-menu-option', - }); - await alice1.clickOnElementAll(new Contact(alice1, 'Bob')); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'qa-collapsing-footer-action_promote', - }); - await alice1.checkModalStrings( - englishStrippedStr('promote').toString(), - englishStrippedStr('adminPromoteDescription').withArgs({ name: bob.userName }).toString() - ); - await alice1.waitForTextElementToBePresent({ - strategy: '-android uiautomator', - selector: `new UiSelector().text("${englishStrippedStr('promoteAdminsWarning').toString()}")`, - }); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'Promote', - }); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'Confirm', + await test.step(`${alice.userName} promotes ${bob.userName}`, async () => { + // Navigate to Promote Members screen + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await alice1.clickOnElementAll(new PromoteMembersMenuItem(alice1)); + await alice1.clickOnElementAll(new Contact(alice1, 'Bob')); + await alice1.clickOnElementAll(new PromoteMemberFooterButton(alice1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Promote'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('promote').toString(), + englishStrippedStr('adminPromoteDescription').withArgs({ name: bob.userName }).toString() + ); + // This is a string that's part of the modal but not part of the modal description element + await alice1.waitForTextElementToBePresent({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('promoteAdminsWarning').toString()}")`, + }); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Confirm Promotion'), async () => { + await alice1.clickOnElementAll(new PromoteMemberModalConfirm(alice1)); + await alice1.checkModalStrings( + englishStrippedStr('confirmPromotion').toString(), + englishStrippedStr('confirmPromotionDescription').toString() + ); + }); + await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); }); await alice1.navigateBack(); await alice1.navigateBack(); - await Promise.all( - [alice1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('adminPromotedToAdmin').withArgs({ name: bob.userName }).toString(), - 30_000 + await test.step('Verify every member sees the promotion control message', async () => { + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('adminPromotedToAdmin').withArgs({ name: bob.userName }).toString(), + 30_000 + ) ) - ) - ); - await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); - // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); - await Promise.all( - [alice1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('disappearingMessagesSet') - .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) - .toString() + ); + await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); + }); + await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { + // Check to see if Bob has admin powers by setting disappearing messages + await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) + .toString() + ) ) - ) - ); - await bob1.waitForControlMessageToBePresent( - englishStrippedStr('disappearingMessagesSetYou') - .withArgs({ time, disappearing_messages_type: 'sent' }) - .toString() - ); - await closeApp(alice1, bob1, charlie1); + ); + await bob1.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: 'sent' }) + .toString() + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); } diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 998789a6d..713e68269 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -6,6 +6,20 @@ import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { StrategyExtractionObj } from '../../../types/testing'; import { GROUPNAME } from '../../../types/testing'; +export class ConfirmPromotionModalButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Confirm', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + export class ConfirmRemovalButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -153,7 +167,6 @@ export class GroupMember extends LocatorsInterface { } } } - export class GroupNameInput extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -186,6 +199,19 @@ export class InviteContactConfirm extends LocatorsInterface { } } } +export class InviteContactSendInviteButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Send Invite', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} export class LatestReleaseBanner extends LocatorsInterface { public build() { switch (this.platform) { @@ -222,6 +248,7 @@ export class LeaveGroupConfirm extends LocatorsInterface { } } } + export class LeaveGroupMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -238,6 +265,21 @@ export class LeaveGroupMenuItem extends LocatorsInterface { } } } + +export class ManageAdminsMenuItem extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'manage-admins-menu-option', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + export class ManageMembersMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -274,6 +316,47 @@ export class MemberStatus extends LocatorsInterface { } } +export class PromoteMemberFooterButton extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'qa-collapsing-footer-action_promote', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + +export class PromoteMemberModalConfirm extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'Promote', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} +export class PromoteMembersMenuItem extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'promote-members-menu-option', + } as const; + case 'ios': + throw new Error('Manage Members not available on iOS'); + } + } +} + export class RemoveMemberButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -290,7 +373,6 @@ export class RemoveMemberButton extends LocatorsInterface { } } } - export class RemoveMemberMessagesRadial extends LocatorsInterface { public build() { switch (this.platform) { @@ -317,6 +399,7 @@ export class RemoveMemberRadial extends LocatorsInterface { } } } + export class SaveGroupNameChangeButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { From 8c7c114d060835fa2879b5cdffb2224293af5b51 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 21 Jan 2026 10:41:39 +1100 Subject: [PATCH 018/184] wip: continue adding test.steps --- .../specs/group_tests_add_accountid.spec.ts | 60 +++++++++++-------- .../specs/group_tests_kick_member.spec.ts | 12 ++-- run/types/allure.ts | 1 + 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 55bef893c..70699dc3e 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteAccountIDOrONS } from './locators'; @@ -41,39 +42,48 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T const { devices: { alice1, bob1, charlie1, unknown1 }, prebuilt: { alice }, - } = await open_Alice1_Bob1_Charlie1_Unknown1({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo: testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_Unknown1({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo: testInfo, + }); + }); + const userD = await test.step(TestSteps.SETUP.NEW_USER, async () => { + return newUser(unknown1, USERNAME.DRACULA); }); const aliceTruncatedPubkey = truncatePubkey(alice.sessionId, platform); const historicMsg = `Hello from ${alice.userName}`; - await alice1.sendMessage(historicMsg); - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) - ) - ); - const userD = await newUser(unknown1, USERNAME.DRACULA); const userDTruncatedPubkey = truncatePubkey(userD.accountID, platform); const userDMsg = `Hello from ${userD.userName}`; - // Click more options - await alice1.clickOnElementAll(new ConversationSettings(alice1)); - // Select edit group - await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); - await sleepFor(1000); - // Add contact to group - await alice1.clickOnElementAll(new InviteAccountIDOrONS(alice1)); - await alice1.inputText(userD.accountID, new EnterAccountID(alice1)); - await alice1.clickOnElementAll(new NextButton(alice1)); - await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); - await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, 'group'), async () => { + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); + }); + await test.step(TestSteps.USER_ACTIONS.GROUPS_ADD_CONTACT(userD.userName), async () => { + // Click more options + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + // Select edit group + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await sleepFor(1000); + // Add contact to group + await alice1.clickOnElementAll(new InviteAccountIDOrONS(alice1)); + await alice1.inputText(userD.accountID, new EnterAccountID(alice1)); + await alice1.clickOnElementAll(new NextButton(alice1)); + await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); + }); // Leave Manage Members await alice1.navigateBack(); // Leave Conversation Settings await alice1.navigateBack(); // Check control messages + await test.step('Verify group invite control message for all members', async () => { await Promise.all( [alice1, bob1, charlie1].map(device => device.waitForControlMessageToBePresent( @@ -82,6 +92,8 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T ) ) ); +}); + await unknown1.clickOnElementAll(new MessageRequestsBanner(unknown1)); await unknown1.clickOnElementAll(new MessageRequestItem(unknown1)); await unknown1.waitForControlMessageToBePresent( diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index 6697cc01f..598251540 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -65,14 +65,10 @@ async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() ), ]); - await bob1 - .onAndroid() - .waitForTextElementToBePresent({ - ...new EmptyConversation(bob1).build(), - text: englishStrippedStr('groupRemovedYou') - .withArgs({ group_name: testGroupName }) - .toString(), - }); + await bob1.onAndroid().waitForTextElementToBePresent({ + ...new EmptyConversation(bob1).build(), + text: englishStrippedStr('groupRemovedYou').withArgs({ group_name: testGroupName }).toString(), + }); await bob1.onIOS().waitForTextElementToBePresent(new EmptyConversation(bob1)); // Message input should not be present after being kicked await bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1000 }); diff --git a/run/types/allure.ts b/run/types/allure.ts index 2f9ff1430..2ba410ea9 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -95,6 +95,7 @@ export const TestSteps = { CHANGE_PROFILE_PICTURE: 'Change profile picture', APP_DISGUISE: 'Set App Disguise', DELETE_FOR_EVERYONE: 'Delete for everyone', + GROUPS_ADD_CONTACT: (name: string) => `Invite ${name} to group` }, // Disappearing Messages DISAPPEARING_MESSAGES: { From 46dd1097e9a348303e205648043f865b236fda3c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 22 Jan 2026 10:19:16 +1100 Subject: [PATCH 019/184] chore: un-break ios 18 simulators --- ci-simulators.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ci-simulators.json b/ci-simulators.json index 1b9e9aff8..f7d00e291 100644 --- a/ci-simulators.json +++ b/ci-simulators.json @@ -1,62 +1,62 @@ [ { "name": "Auto-16PM-0", - "udid": "4D0EDB6B-9517-4E9F-AD80-4853604401FB", + "udid": "640934E9-EAEE-4381-B471-E63C2D8A537A", "wdaPort": 1253 }, { "name": "Auto-16PM-1", - "udid": "145BA489-0AAB-473F-9238-57C8AD75576A", + "udid": "865FC54C-642A-4261-B495-94319C048243", "wdaPort": 1254 }, { "name": "Auto-16PM-2", - "udid": "6F8F94E6-5623-4C8C-88A0-DAE34F343BCE", + "udid": "5E6B8EF5-DC61-4372-A592-144A8B0BDAA1", "wdaPort": 1255 }, { "name": "Auto-16PM-3", - "udid": "5CFFE21B-26BE-4636-99FE-B5D7B8DC76C4", + "udid": "061D5614-467F-48B9-AA6E-B5219D8371CB", "wdaPort": 1256 }, { "name": "Auto-16PM-4", - "udid": "570FEA9F-AFC2-4CCE-B637-290D0EE290C4", + "udid": "2499A000-AF75-4A5F-9D4C-7B88C00BC210", "wdaPort": 1257 }, { "name": "Auto-16PM-5", - "udid": "09D47861-AF97-4D56-9DC1-9839168AA3CA", + "udid": "74BD32ED-1AD0-476D-990A-F83AB37C85BB", "wdaPort": 1258 }, { "name": "Auto-16PM-6", - "udid": "3C7A031A-3224-40A9-86C7-BE64B8B6E0A2", + "udid": "34AA0517-BB14-4B9C-A81B-29EBC31BE061", "wdaPort": 1259 }, { "name": "Auto-16PM-7", - "udid": "BA458AF8-C3F9-41E7-8B76-61157EA5EDF3", + "udid": "C65632CE-913C-42E2-B0EF-5E47A122556E", "wdaPort": 1260 }, { "name": "Auto-16PM-8", - "udid": "5C799A8A-2AE0-4ED9-A077-BCC703ABF7E0", + "udid": "35879E78-64E1-4605-A816-EFF1D6F0336D", "wdaPort": 1261 }, { "name": "Auto-16PM-9", - "udid": "AEE0AE84-26FA-42FE-85CD-82780DF1154C", + "udid": "991B45A5-C3BD-4421-A4D3-74C58618BEDB", "wdaPort": 1262 }, { "name": "Auto-16PM-10", - "udid": "5B947D6C-DAE0-4066-9263-C2B3E1B4E970", + "udid": "48458DAE-FDE7-404D-9C12-FA35A54A8356", "wdaPort": 1263 }, { "name": "Auto-16PM-11", - "udid": "662F717D-A26D-47C7-A47B-E5090B1C4239", + "udid": "A56E335C-3F02-425F-80E3-FE9AE1C0064C", "wdaPort": 1264 } ] \ No newline at end of file From d790f3766232d4da8adc979b7c3be0e93927d329 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 27 Jan 2026 09:52:58 +1100 Subject: [PATCH 020/184] fix: force ipv4 in the qa seeder --- package.json | 1 + run/test/specs/state_builder/index.ts | 6 ++++++ yarn.lock | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 5c4d51987..39616cb75 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.15.0", + "undici": "^7.19.1", "wd": "^1.14.0", "wdio-wait-for": "^2.2.6" }, diff --git a/run/test/specs/state_builder/index.ts b/run/test/specs/state_builder/index.ts index 36cfbd44a..9944a20b5 100644 --- a/run/test/specs/state_builder/index.ts +++ b/run/test/specs/state_builder/index.ts @@ -1,3 +1,9 @@ +import { Agent, setGlobalDispatcher } from 'undici'; + +// Force IPv4 connections to work around Node.js fetch/undici lacking "Happy Eyeballs" (RFC 6555) +// https://github.com/node-fetch/node-fetch/issues/1297 +setGlobalDispatcher(new Agent({ connect: { family: 4 } })); + import type { TestInfo } from '@playwright/test'; import { diff --git a/yarn.lock b/yarn.lock index 42b5d30db..5709bbad4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6816,6 +6816,7 @@ __metadata: ts-node: "npm:^10.9.1" typescript: "npm:^5.6.3" typescript-eslint: "npm:^8.15.0" + undici: "npm:^7.19.1" wd: "npm:^1.14.0" wdio-wait-for: "npm:^2.2.6" languageName: unknown @@ -7777,6 +7778,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.19.1": + version: 7.19.1 + resolution: "undici@npm:7.19.1" + checksum: 10c0/3807f9968dfbbdb45da8e6d68279f2831ff9a173e01f116eb3d43aa07fb03474aae307aba0d167a55ad686ec21733d357776c73639a26ecaaf877cbd6b904e79 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" From d105075d3999f525918a68831d6f5471e7a4edbe Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 27 Jan 2026 11:49:28 +1100 Subject: [PATCH 021/184] chore: add test steps to manage member tests --- .../specs/group_tests_add_accountid.spec.ts | 58 ++++---- .../specs/group_tests_add_contact.spec.ts | 124 +++++++++-------- .../group_tests_add_contact_nohistory.spec.ts | 128 ++++++++++-------- .../group_tests_admin_leave_group.spec.ts | 91 +++++++++++++ .../specs/group_tests_delete_group.spec.ts | 7 +- .../specs/group_tests_kick_member.spec.ts | 89 ++++++------ .../group_tests_kick_member_messages.spec.ts | 124 +++++++++-------- run/test/specs/locators/groups.ts | 23 +++- run/types/allure.ts | 4 +- run/types/testing.ts | 5 +- 10 files changed, 414 insertions(+), 239 deletions(-) create mode 100644 run/test/specs/group_tests_admin_leave_group.spec.ts diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 70699dc3e..6b8589010 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -36,7 +36,6 @@ androidIt({ 'Verifies that inviting a non-contact Account ID (without chat history) works as expected.', }); -// TODO proper locator classes, test.steps async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Group to test adding contact'; const { @@ -84,31 +83,36 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T await alice1.navigateBack(); // Check control messages await test.step('Verify group invite control message for all members', async () => { - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('groupMemberNew').withArgs({ name: userDTruncatedPubkey }).toString(), - 20_000 + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberNew').withArgs({ name: userDTruncatedPubkey }).toString(), + 20_000 + ) ) - ) - ); -}); - - await unknown1.clickOnElementAll(new MessageRequestsBanner(unknown1)); - await unknown1.clickOnElementAll(new MessageRequestItem(unknown1)); - await unknown1.waitForControlMessageToBePresent( - englishStrippedStr('messageRequestGroupInvite') - .withArgs({ name: aliceTruncatedPubkey, group_name: testGroupName }) - .toString() - ); - await unknown1.clickOnElementAll(new AcceptMessageRequestButton(unknown1)); - await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); - await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); - await unknown1.sendMessage(userDMsg); - await Promise.all( - [alice1, bob1, charlie1, unknown1].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, userDMsg)) - ) - ); - await closeApp(alice1, bob1, charlie1, unknown1); + ); + }); + await test.step(`${userD.userName} accepts group invite and sends a message`, async () => { + await unknown1.clickOnElementAll(new MessageRequestsBanner(unknown1)); + await unknown1.clickOnElementAll(new MessageRequestItem(unknown1)); + await unknown1.waitForControlMessageToBePresent( + englishStrippedStr('messageRequestGroupInvite') + .withArgs({ name: aliceTruncatedPubkey, group_name: testGroupName }) + .toString() + ); + await unknown1.clickOnElementAll(new AcceptMessageRequestButton(unknown1)); + await unknown1.waitForControlMessageToBePresent( + englishStrippedStr('groupInviteYou').toString() + ); + await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.sendMessage(userDMsg); + await Promise.all( + [alice1, bob1, charlie1, unknown1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, userDMsg)) + ) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1, unknown1); + }); } diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index 2abc00a49..ecf22e38d 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteContactsMenuItem } from './locators'; @@ -32,65 +33,80 @@ androidIt({ 'Verifies that inviting a contact to a group with message history works as expected.', }); -// TODO proper locator classes, test.steps async function addContactToGroupHistory(platform: SupportedPlatformsType, testInfo: TestInfo) { - const testGroupName = 'Group to test adding contact'; + const testGroupName = 'Test group'; const { devices: { alice1, bob1, charlie1, unknown1 }, prebuilt: { alice, group }, - } = await open_Alice1_Bob1_Charlie1_Unknown1({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo: testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_Unknown1({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo: testInfo, + }); }); const historicMsg = `Hello from ${alice.userName}`; - await alice1.sendMessage(historicMsg); - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) - ) - ); - const userD = await newUser(unknown1, USERNAME.DRACULA); - await alice1.navigateBack(); - await newContact(platform, alice1, alice, unknown1, userD); - // Exit to conversation list - await alice1.navigateBack(); - // Select group conversation in list - await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); - // Click more options - await alice1.clickOnElementAll(new ConversationSettings(alice1)); - // Select edit group - await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); - await sleepFor(1000); - // Add contact to group - await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); - // Select new user - await alice1.clickOnElementAll({ - ...new Contact(alice1).build(), - text: USERNAME.DRACULA, + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testGroupName), async () => { + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); + }); + const userD = await test.step(TestSteps.SETUP.NEW_USER, async () => { + return newUser(unknown1, USERNAME.DRACULA); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, userD.userName), async () => { + await alice1.navigateBack(); + await newContact(platform, alice1, alice, unknown1, userD); + // Exit to conversation list + await alice1.navigateBack(); }); - await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); - await alice1.clickOnElementAll(new ShareMessageHistoryRadial(alice1)); - await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); - // Leave Manage Members - await alice1.navigateBack(); - // Leave Conversation Settings - await alice1.navigateBack(); - // Check control messages - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('groupMemberNew').withArgs({ name: USERNAME.DRACULA }).toString() + await test.step(`${alice.userName} invites ${userD.userName} to the group (with message history)`, async () => { + // Select group conversation in list + await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); + // Click more options + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + // Select edit group + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await sleepFor(1000); + // Add contact to group + await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); + // Select new user + await alice1.clickOnElementAll({ + ...new Contact(alice1).build(), + text: USERNAME.DRACULA, + }); + await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); + await alice1.clickOnElementAll(new ShareMessageHistoryRadial(alice1)); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); + }); + await test.step(`Verify ${userD.userName} becomes a fully-fledged member and sees historic messages`, async () => { + // Leave Manage Members + await alice1.navigateBack(); + // Leave Conversation Settings + await alice1.navigateBack(); + // Check control messages + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberNew').withArgs({ name: USERNAME.DRACULA }).toString() + ) ) - ) - ); - // Leave conversation - await unknown1.navigateBack(); - // Leave Message Requests screen - await unknown1.navigateBack(); - await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 - await unknown1.waitForTextElementToBePresent(new MessageBody(unknown1, historicMsg)); - await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); - await closeApp(alice1, bob1, charlie1, unknown1); + ); + // Leave conversation + await unknown1.navigateBack(); + // Leave Message Requests screen + await unknown1.navigateBack(); + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 + await unknown1.waitForTextElementToBePresent(new MessageBody(unknown1, historicMsg)); + await unknown1.waitForControlMessageToBePresent( + englishStrippedStr('groupInviteYou').toString() + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1, unknown1); + }); } diff --git a/run/test/specs/group_tests_add_contact_nohistory.spec.ts b/run/test/specs/group_tests_add_contact_nohistory.spec.ts index 1287c56c4..1c7a0e2ea 100644 --- a/run/test/specs/group_tests_add_contact_nohistory.spec.ts +++ b/run/test/specs/group_tests_add_contact_nohistory.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { InviteContactsMenuItem } from './locators'; @@ -32,67 +33,82 @@ bothPlatformsIt({ 'Verifies that inviting a contact (Android: without chat history) works as expected.', }); -// TODO proper locator classes, test.steps async function addContactToGroupNoHistory(platform: SupportedPlatformsType, testInfo: TestInfo) { - const testGroupName = 'Group to test adding contact'; + const testGroupName = 'Test group'; const { devices: { alice1, bob1, charlie1, unknown1 }, prebuilt: { alice, group }, - } = await open_Alice1_Bob1_Charlie1_Unknown1({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo: testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_Unknown1({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo: testInfo, + }); }); const historicMsg = `Hello from ${alice.userName}`; - await alice1.sendMessage(historicMsg); - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) - ) - ); - const userD = await newUser(unknown1, USERNAME.DRACULA); - await alice1.navigateBack(); - await newContact(platform, alice1, alice, unknown1, userD); - // Exit to conversation list - await alice1.navigateBack(); - // Select group conversation in list - await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); - // Click more options - await alice1.clickOnElementAll(new ConversationSettings(alice1)); - // Select edit group - await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); - await sleepFor(1000); - // Add contact to group - await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); - // Select new user - await alice1.clickOnElementAll({ - ...new Contact(alice1).build(), - text: USERNAME.DRACULA, + await test.step(TestSteps.SEND.MESSAGE(alice.userName, testGroupName), async () => { + await alice1.sendMessage(historicMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, historicMsg)) + ) + ); + }); + const userD = await test.step(TestSteps.SETUP.NEW_USER, async () => { + return newUser(unknown1, USERNAME.DRACULA); + }); + await test.step(TestSteps.SEND.MESSAGE(alice.userName, userD.userName), async () => { + await alice1.navigateBack(); + await newContact(platform, alice1, alice, unknown1, userD); + // Exit to conversation list + await alice1.navigateBack(); }); - await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); - if (platform === 'android') { - await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); - await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); - } - // Leave Manage Members - await alice1.navigateBack(); - // Leave Conversation Settings - await alice1.navigateBack(); - // Check control messages - await Promise.all( - [alice1, bob1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('groupMemberNew').withArgs({ name: USERNAME.DRACULA }).toString() + await test.step(`${alice.userName} invites ${userD.userName} to the group (without message history)`, async () => { + // Select group conversation in list + await alice1.clickOnElementAll(new ConversationItem(alice1, testGroupName)); + // Click more options + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + // Select edit group + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await sleepFor(1000); + // Add contact to group + await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); + // Select new user + await alice1.clickOnElementAll({ + ...new Contact(alice1).build(), + text: USERNAME.DRACULA, + }); + await alice1.clickOnElementAll(new InviteContactConfirm(alice1)); + if (platform === 'android') { + await alice1.clickOnElementAll(new ShareNewMessagesRadial(alice1)); + await alice1.clickOnElementAll(new InviteContactSendInviteButton(alice1)); + } + }); + await test.step(`Verify ${userD.userName} becomes a fully-fledged member and doesn't see historic messages`, async () => { + // Leave Manage Members + await alice1.navigateBack(); + // Leave Conversation Settings + await alice1.navigateBack(); + // Check control messages + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberNew').withArgs({ name: USERNAME.DRACULA }).toString() + ) ) - ) - ); - // Leave conversation - await unknown1.navigateBack(); - // Leave Message Requests screen (Android) - await unknown1.onAndroid().navigateBack(); - await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 - await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); - await unknown1.waitForControlMessageToBePresent(englishStrippedStr('groupInviteYou').toString()); - await closeApp(alice1, bob1, charlie1, unknown1); + ); + // Leave conversation + await unknown1.navigateBack(); + // Leave Message Requests screen (Android) + await unknown1.onAndroid().navigateBack(); + await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 + await unknown1.verifyElementNotPresent(new MessageBody(unknown1, historicMsg)); + await unknown1.waitForControlMessageToBePresent( + englishStrippedStr('groupInviteYou').toString() + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1, unknown1); + }); } diff --git a/run/test/specs/group_tests_admin_leave_group.spec.ts b/run/test/specs/group_tests_admin_leave_group.spec.ts new file mode 100644 index 000000000..6e2ba9996 --- /dev/null +++ b/run/test/specs/group_tests_admin_leave_group.spec.ts @@ -0,0 +1,91 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { ConversationSettings, EmptyConversation } from './locators/conversation'; +import { + DeleteGroupConfirm, + LeaveGroupCancel, + LeaveGroupConfirm, + LeaveGroupMenuItem, +} from './locators/groups'; +import { ConversationItem, PlusButton } from './locators/home'; +import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; + +androidIt({ + title: 'Leave group as the only admin', + risk: 'high', + testCb: deleteGroup, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Groups', + suite: 'Leave/Delete Group', + }, + allureDescription: `Verifies that a solo admin can't leave a group but is instead prompted to add admins or delete the group.`, +}); + +async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Leave group'; + const { + devices: { alice1, bob1, charlie1 }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + }); + await test.step('Admin attempts to leave group', async () => { + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new LeaveGroupMenuItem(alice1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Leave Group'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('groupLeave').toString(), + englishStrippedStr('groupOnlyAdminLeave').withArgs({ group_name: testGroupName }).toString() + ); + // Seems like this modal still has the leave group qa-tags so we're making sure they're the right text + await alice1.waitForTextElementToBePresent({ + ...new LeaveGroupConfirm(alice1).build(), + text: englishStrippedStr('addAdmin').withArgs({ count: 1 }).toString(), + }); + await alice1.waitForTextElementToBePresent({ + ...new LeaveGroupCancel(alice1).build(), + text: englishStrippedStr('groupDelete').toString(), + }); + }); + await alice1.clickOnElementAll(new LeaveGroupCancel(alice1)); + }); + await test.step('Admin deletes group from Leave Group modal', async () => { + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Delete Group'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('groupDelete').toString(), + englishStrippedStr('groupDeleteDescription') + .withArgs({ group_name: testGroupName }) + .toString() + ); + }); + await alice1.clickOnElementAll(new DeleteGroupConfirm(alice1)); + }); + await test.step(TestSteps.VERIFY.GROUP_DELETED, async () => { + // Android uses the empty state for this "control message" + await Promise.all( + [bob1, charlie1].map(device => + device.waitForTextElementToBePresent({ + ...new EmptyConversation(device).build(), + text: englishStrippedStr('groupDeletedMemberDescription') + .withArgs({ group_name: testGroupName }) + .toString(), + }) + ) + ); + await alice1.waitForTextElementToBePresent(new PlusButton(alice1)); // Ensure we're on the home screen + await alice1.verifyElementNotPresent(new ConversationItem(alice1, testGroupName).build()); + }); + + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); +} diff --git a/run/test/specs/group_tests_delete_group.spec.ts b/run/test/specs/group_tests_delete_group.spec.ts index c260824a3..55d3fb6bd 100644 --- a/run/test/specs/group_tests_delete_group.spec.ts +++ b/run/test/specs/group_tests_delete_group.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { ConversationSettings } from './locators/conversation'; +import { ConversationSettings, EmptyConversation } from './locators/conversation'; import { DeleteGroupConfirm, DeleteGroupMenuItem } from './locators/groups'; import { ConversationItem, PlusButton } from './locators/home'; import { open_Alice2_Bob1_Charlie1_friends_group } from './state_builder'; @@ -47,7 +47,7 @@ async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) }); await alice1.clickOnElementAll(new DeleteGroupConfirm(alice1)); }); - await test.step('Verify group is deleted for all members', async () => { + await test.step(TestSteps.VERIFY.GROUP_DELETED, async () => { // Members if (platform === 'ios') { await Promise.all( @@ -64,8 +64,7 @@ async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) await Promise.all( [bob1, charlie1].map(device => device.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Empty list', + ...new EmptyConversation(device).build(), text: englishStrippedStr('groupDeletedMemberDescription') .withArgs({ group_name: testGroupName }) .toString(), diff --git a/run/test/specs/group_tests_kick_member.spec.ts b/run/test/specs/group_tests_kick_member.spec.ts index 598251540..df7cac9bd 100644 --- a/run/test/specs/group_tests_kick_member.spec.ts +++ b/run/test/specs/group_tests_kick_member.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ConversationSettings, EmptyConversation, MessageInput } from './locators/conversation'; @@ -11,7 +12,7 @@ import { RemoveMemberButton, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; -import { SupportedPlatformsType } from './utils/open_app'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ title: 'Kick member', @@ -26,50 +27,60 @@ bothPlatformsIt({ 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group.', }); -// TODO proper locator classes, test.steps async function kickMember(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Kick member'; - const { devices: { alice1, bob1, charlie1 }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, + prebuilt: { bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); }); - await alice1.clickOnElementAll(new ConversationSettings(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( - englishStrippedStr('remove').toString(), - englishStrippedStr('groupRemoveDescription') - .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) - .toString() - ); - await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); - // The Group Member element sometimes disappears slowly, sometimes quickly. - // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore - await alice1.verifyElementNotPresent({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 5_000, + await test.step(TestSteps.USER_ACTIONS.GROUPS_REMOVE_MEMBER(bob.userName), async () => { + await alice1.clickOnElementAll(new ConversationSettings(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( + englishStrippedStr('remove').toString(), + englishStrippedStr('groupRemoveDescription') + .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) + .toString() + ); + await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + await alice1.verifyElementNotPresent({ + ...new GroupMember(alice1).build(USERNAME.BOB), + maxWait: 5_000, + }); }); await alice1.navigateBack(); await alice1.navigateBack(); - await Promise.all([ - alice1.waitForControlMessageToBePresent( - englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() - ), - charlie1.waitForControlMessageToBePresent( - englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() - ), - ]); - await bob1.onAndroid().waitForTextElementToBePresent({ - ...new EmptyConversation(bob1).build(), - text: englishStrippedStr('groupRemovedYou').withArgs({ group_name: testGroupName }).toString(), + await test.step(`Verify ${bob.userName} has been kicked`, async () => { + await Promise.all([ + alice1.waitForControlMessageToBePresent( + englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() + ), + charlie1.waitForControlMessageToBePresent( + englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() + ), + ]); + await bob1.onAndroid().waitForTextElementToBePresent({ + ...new EmptyConversation(bob1).build(), + text: englishStrippedStr('groupRemovedYou') + .withArgs({ group_name: testGroupName }) + .toString(), + }); + await bob1.onIOS().waitForTextElementToBePresent(new EmptyConversation(bob1)); + // Message input should not be present after being kicked + await bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1000 }); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); }); - await bob1.onIOS().waitForTextElementToBePresent(new EmptyConversation(bob1)); - // Message input should not be present after being kicked - await bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1000 }); } diff --git a/run/test/specs/group_tests_kick_member_messages.spec.ts b/run/test/specs/group_tests_kick_member_messages.spec.ts index efcdb34da..547f60d5c 100644 --- a/run/test/specs/group_tests_kick_member_messages.spec.ts +++ b/run/test/specs/group_tests_kick_member_messages.spec.ts @@ -1,6 +1,7 @@ -import type { TestInfo } from '@playwright/test'; +import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { @@ -18,7 +19,7 @@ import { RemoveMemberMessagesRadial, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; -import { SupportedPlatformsType } from './utils/open_app'; +import { closeApp, SupportedPlatformsType } from './utils/open_app'; // This functionality only exists on Android at the moment androidIt({ @@ -34,66 +35,83 @@ androidIt({ 'Verifies that a group member can be kicked from a group and that the kicked member is removed from the group (with their messages deleted).', }); -// TODO proper locator classes, test.steps async function kickMemberDeleteMsg(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Kick member'; - const { devices: { alice1, bob1, charlie1 }, prebuilt: { alice, bob }, - } = await open_Alice1_Bob1_Charlie1_friends_group({ - platform, - groupName: testGroupName, - focusGroupConvo: true, - testInfo, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); }); const aliceMsg = `Hello I am ${alice.userName}`; const bobMsg = `Hello I am ${bob.userName}`; - await alice1.sendMessage(aliceMsg); - await bob1.sendMessage(bobMsg); - await alice1.clickOnElementAll(new ConversationSettings(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( - englishStrippedStr('remove').toString(), - englishStrippedStr('groupRemoveDescription') - .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) - .toString() - ); - await alice1.clickOnElementAll(new RemoveMemberMessagesRadial(alice1)); - await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); - // The Group Member element sometimes disappears slowly, sometimes quickly. - // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore - await alice1.verifyElementNotPresent({ - ...new GroupMember(alice1).build(USERNAME.BOB), - maxWait: 5_000, + await test.step(`${alice.userName} and ${bob.userName} send a message to the group`, async () => { + await alice1.sendMessage(aliceMsg); + await bob1.sendMessage(bobMsg); + await Promise.all( + [alice1, bob1, charlie1].map(async device => { + await device.waitForTextElementToBePresent(new MessageBody(device, aliceMsg)); + await device.waitForTextElementToBePresent(new MessageBody(device, bobMsg)); + }) + ); + }); + await test.step(TestSteps.USER_ACTIONS.GROUPS_REMOVE_MEMBER(bob.userName), async () => { + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageMembersMenuItem(alice1)); + await alice1.clickOnElementAll({ ...new GroupMember(alice1).build(USERNAME.BOB) }); + await alice1.clickOnElementAll(new RemoveMemberButton(alice1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Remove Member'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('remove').toString(), + englishStrippedStr('groupRemoveDescription') + .withArgs({ name: USERNAME.BOB, group_name: testGroupName }) + .toString() + ); + }); + await alice1.clickOnElementAll(new RemoveMemberMessagesRadial(alice1)); + await alice1.clickOnElementAll(new ConfirmRemovalButton(alice1)); + // The Group Member element sometimes disappears slowly, sometimes quickly. + // hasElementBeenDeleted would be theoretically better but we just check if element is not there anymore + await alice1.verifyElementNotPresent({ + ...new GroupMember(alice1).build(USERNAME.BOB), + maxWait: 5_000, + }); }); await alice1.navigateBack(); await alice1.navigateBack(); - await Promise.all( - [alice1, charlie1].map(async device => { - await device.waitForControlMessageToBePresent( - englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() - ); - await device.waitForTextElementToBePresent(new MessageBody(device, aliceMsg)); - await device.verifyElementNotPresent({ - ...new MessageBody(device, bobMsg).build(), - maxWait: 1_000, - }); - await device.waitForTextElementToBePresent(new DeletedMessage(device)); - }) - ); - await Promise.all([ - bob1.waitForTextElementToBePresent({ - ...new EmptyConversation(bob1).build(), - text: englishStrippedStr('groupRemovedYou') - .withArgs({ group_name: testGroupName }) - .toString(), - }), - bob1.verifyElementNotPresent(new MessageBody(bob1, aliceMsg)), - bob1.verifyElementNotPresent(new MessageBody(bob1, bobMsg)), - bob1.verifyElementNotPresent(new DeletedMessage(bob1)), - bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1_000 }), - ]); + await test.step(`Verify ${bob.userName} has been kicked and his message has been deleted`, async () => { + await Promise.all( + [alice1, charlie1].map(async device => { + await device.waitForControlMessageToBePresent( + englishStrippedStr('groupRemoved').withArgs({ name: USERNAME.BOB }).toString() + ); + await device.waitForTextElementToBePresent(new MessageBody(device, aliceMsg)); + await device.verifyElementNotPresent({ + ...new MessageBody(device, bobMsg).build(), + maxWait: 1_000, + }); + await device.waitForTextElementToBePresent(new DeletedMessage(device)); + }) + ); + await Promise.all([ + bob1.waitForTextElementToBePresent({ + ...new EmptyConversation(bob1).build(), + text: englishStrippedStr('groupRemovedYou') + .withArgs({ group_name: testGroupName }) + .toString(), + }), + bob1.verifyElementNotPresent(new MessageBody(bob1, aliceMsg)), + bob1.verifyElementNotPresent(new MessageBody(bob1, bobMsg)), + bob1.verifyElementNotPresent(new DeletedMessage(bob1)), + bob1.verifyElementNotPresent({ ...new MessageInput(bob1).build(), maxWait: 1_000 }), + ]); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); } diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 21e0732eb..db94bc6e8 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -232,13 +232,32 @@ export class LatestReleaseBanner extends LocatorsInterface { } } } +export class LeaveGroupCancel extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("leave-group-cancel-button").childSelector(new UiSelector().className("android.widget.TextView"))', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Cancel', + }; + } + } +} + export class LeaveGroupConfirm extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'leave-group-confirm-button', + strategy: '-android uiautomator', + selector: + 'new UiSelector().resourceId("leave-group-confirm-button").childSelector(new UiSelector().className("android.widget.TextView"))', } as const; case 'ios': return { diff --git a/run/types/allure.ts b/run/types/allure.ts index 89b52382b..086411a16 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -95,7 +95,8 @@ export const TestSteps = { CHANGE_PROFILE_PICTURE: 'Change profile picture', APP_DISGUISE: 'Set App Disguise', DELETE_FOR_EVERYONE: 'Delete for everyone', - GROUPS_ADD_CONTACT: (name: string) => `Invite ${name} to group` + GROUPS_ADD_CONTACT: (name: string) => `Invite ${name} to group`, + GROUPS_REMOVE_MEMBER: (name: string) => `Remove ${name} from group`, }, // Disappearing Messages DISAPPEARING_MESSAGES: { @@ -121,5 +122,6 @@ export const TestSteps = { NICKNAME_CHANGED: (context: string) => `Verify nickname changed in/on ${context}`, PROFILE_PICTURE_CHANGED: 'Verify profile picture has been changed', EMOJI_REACT: 'Verify emoji react appears for everyone', + GROUP_DELETED: 'Verify group is deleted for all members', }, }; diff --git a/run/types/testing.ts b/run/types/testing.ts index 928e96b32..aba5b9eb3 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -13,7 +13,6 @@ export const USERNAME = usernameFromSeeder; export type GROUPNAME = | 'Disappear after send test' | 'Disappear after sent test' - | 'Group to test adding contact' | 'Kick member' | 'Leave group' | 'Leave group linked device' @@ -159,14 +158,13 @@ export type XPath = | `/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.TabHost/android.widget.LinearLayout/android.widget.FrameLayout/androidx.viewpager.widget.ViewPager/android.widget.RelativeLayout/android.widget.GridView/android.widget.LinearLayout/android.widget.LinearLayout[2]`; export type UiAutomatorQuery = - | 'new UiSelector().resourceId("cta-button-negative").childSelector(new UiSelector().className("android.widget.TextView"))' - | 'new UiSelector().resourceId("cta-button-positive").childSelector(new UiSelector().className("android.widget.TextView"))' | 'new UiSelector().resourceId("network.loki.messenger:id/messageStatusTextView").text("Sent")' | 'new UiSelector().text("Enter your display name")' | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId(${string}))` | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text(${string}))` | `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textStartsWith(${string}))` | `new UiSelector().resourceId("Conversation header name").childSelector(new UiSelector().resourceId("pro-badge-text"))` + | `new UiSelector().resourceId(${string}).childSelector(new UiSelector().className("android.widget.TextView"))` | `new UiSelector().text(${string})`; export type AccessibilityId = @@ -511,6 +509,7 @@ export type Id = | 'Last updated timestamp' | 'Learn about staking link' | 'Learn more link' + | 'leave-group-cancel-button' | 'leave-group-confirm-button' | 'leave-group-menu-option' | 'Leave' From d97b812525fab083cd0c845b2e2b4bdf5cbb8c35 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 28 Jan 2026 16:33:27 +1100 Subject: [PATCH 022/184] feat: add multi admin leave test --- .../group_tests_admin_leave_group.spec.ts | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/run/test/specs/group_tests_admin_leave_group.spec.ts b/run/test/specs/group_tests_admin_leave_group.spec.ts index 6e2ba9996..c794d060d 100644 --- a/run/test/specs/group_tests_admin_leave_group.spec.ts +++ b/run/test/specs/group_tests_admin_leave_group.spec.ts @@ -4,11 +4,17 @@ import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { ConversationSettings, EmptyConversation } from './locators/conversation'; +import { Contact } from './locators/global'; import { + ConfirmPromotionModalButton, DeleteGroupConfirm, LeaveGroupCancel, LeaveGroupConfirm, LeaveGroupMenuItem, + ManageAdminsMenuItem, + PromoteMemberFooterButton, + PromoteMemberModalConfirm, + PromoteMembersMenuItem, } from './locators/groups'; import { ConversationItem, PlusButton } from './locators/home'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; @@ -17,16 +23,30 @@ import { closeApp, SupportedPlatformsType } from './utils/open_app'; androidIt({ title: 'Leave group as the only admin', risk: 'high', - testCb: deleteGroup, + testCb: soloAdminLeave, countOfDevicesNeeded: 3, allureSuites: { parent: 'Groups', suite: 'Leave/Delete Group', }, - allureDescription: `Verifies that a solo admin can't leave a group but is instead prompted to add admins or delete the group.`, + allureDescription: + "Verifies that a solo admin can't leave a group but is instead prompted to add admins or delete the group.", }); -async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { +androidIt({ + title: 'Leave group with more than one admin', + risk: 'medium', + testCb: multiAdminLeave, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Groups', + suite: 'Leave/Delete Group', + }, + allureDescription: + 'Verifies that an admin can leave a group if there is more than one admin in the group.', +}); + +async function soloAdminLeave(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Leave group'; const { devices: { alice1, bob1, charlie1 }, @@ -89,3 +109,65 @@ async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) await closeApp(alice1, bob1, charlie1); }); } + +async function multiAdminLeave(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Test group'; + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { alice, bob }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + }); + await test.step(`${alice.userName} promotes ${bob.userName}`, async () => { + // Navigate to Promote Members screen + await alice1.sendMessage(`Gonna promote ${bob.userName} now`); + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await alice1.clickOnElementAll(new PromoteMembersMenuItem(alice1)); + await alice1.clickOnElementAll(new Contact(alice1, 'Bob')); + await alice1.clickOnElementAll(new PromoteMemberFooterButton(alice1)); + await alice1.clickOnElementAll(new PromoteMemberModalConfirm(alice1)); + await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); + }); + await alice1.navigateBack(); + await alice1.navigateBack(); + await test.step('Verify every member sees the promotion control message', async () => { + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('adminPromotedToAdmin').withArgs({ name: bob.userName }).toString(), + 30_000 + ) + ) + ); + await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Leave Group'), async () => { + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new LeaveGroupMenuItem(alice1)); + await alice1.checkModalStrings( + englishStrippedStr('groupLeave').toString(), + englishStrippedStr('groupLeaveDescription').withArgs({ group_name: testGroupName }).toString() + ); + }); + await alice1.clickOnElementAll(new LeaveGroupConfirm(alice1)); + + await Promise.all( + [bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberLeft').withArgs({ name: alice.userName }).toString(), + 30_000 + ) + ) + ); + await alice1.waitForTextElementToBePresent(new PlusButton(alice1)); + await alice1.verifyElementNotPresent(new ConversationItem(alice1, testGroupName).build()); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); +} From 01435e15a769d08076432cb253c5e9ed99f44c69 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 30 Jan 2026 10:27:41 +1100 Subject: [PATCH 023/184] fix: tidy up multi admin leave test --- .../group_tests_admin_leave_group.spec.ts | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/run/test/specs/group_tests_admin_leave_group.spec.ts b/run/test/specs/group_tests_admin_leave_group.spec.ts index c794d060d..d7f3e9e82 100644 --- a/run/test/specs/group_tests_admin_leave_group.spec.ts +++ b/run/test/specs/group_tests_admin_leave_group.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; -import { ConversationSettings, EmptyConversation } from './locators/conversation'; +import { ConversationSettings, EmptyConversation, MessageBody } from './locators/conversation'; import { Contact } from './locators/global'; import { ConfirmPromotionModalButton, @@ -12,12 +12,14 @@ import { LeaveGroupConfirm, LeaveGroupMenuItem, ManageAdminsMenuItem, + MemberStatus, PromoteMemberFooterButton, PromoteMemberModalConfirm, PromoteMembersMenuItem, } from './locators/groups'; import { ConversationItem, PlusButton } from './locators/home'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { sleepFor } from './utils'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; androidIt({ @@ -123,9 +125,15 @@ async function multiAdminLeave(platform: SupportedPlatformsType, testInfo: TestI testInfo, }); }); + const promoteMsg = `Gonna promote ${bob.userName} now`; + await alice1.sendMessage(promoteMsg); + await Promise.all( + [alice1, bob1, charlie1].map(device => + device.waitForTextElementToBePresent(new MessageBody(device, promoteMsg).build()) + ) + ); await test.step(`${alice.userName} promotes ${bob.userName}`, async () => { - // Navigate to Promote Members screen - await alice1.sendMessage(`Gonna promote ${bob.userName} now`); + // Navigate to Manage Admins screen await alice1.clickOnElementAll(new ConversationSettings(alice1)); await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); await alice1.clickOnElementAll(new PromoteMembersMenuItem(alice1)); @@ -133,9 +141,14 @@ async function multiAdminLeave(platform: SupportedPlatformsType, testInfo: TestI await alice1.clickOnElementAll(new PromoteMemberFooterButton(alice1)); await alice1.clickOnElementAll(new PromoteMemberModalConfirm(alice1)); await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); + // This is not tied to Bob but they're the only admin this status can apply to + await alice1.waitForTextElementToBePresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ); }); await alice1.navigateBack(); await alice1.navigateBack(); + // SES-5178 await test.step('Verify every member sees the promotion control message', async () => { await Promise.all( [alice1, charlie1].map(device => @@ -147,26 +160,36 @@ async function multiAdminLeave(platform: SupportedPlatformsType, testInfo: TestI ); await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); }); - await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Leave Group'), async () => { + await test.step('Verify promotion status is correct', async () => { await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await alice1.verifyElementNotPresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ); + await sleepFor(1_000); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Leave Group'), async () => { + await alice1.navigateBack(); await alice1.clickOnElementAll(new LeaveGroupMenuItem(alice1)); await alice1.checkModalStrings( englishStrippedStr('groupLeave').toString(), englishStrippedStr('groupLeaveDescription').withArgs({ group_name: testGroupName }).toString() ); }); - await alice1.clickOnElementAll(new LeaveGroupConfirm(alice1)); + await test.step(`${alice.userName} leaves the group`, async () => { + await alice1.clickOnElementAll(new LeaveGroupConfirm(alice1)); - await Promise.all( - [bob1, charlie1].map(device => - device.waitForControlMessageToBePresent( - englishStrippedStr('groupMemberLeft').withArgs({ name: alice.userName }).toString(), - 30_000 + await Promise.all( + [bob1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('groupMemberLeft').withArgs({ name: alice.userName }).toString(), + 30_000 + ) ) - ) - ); - await alice1.waitForTextElementToBePresent(new PlusButton(alice1)); - await alice1.verifyElementNotPresent(new ConversationItem(alice1, testGroupName).build()); + ); + await alice1.waitForTextElementToBePresent(new PlusButton(alice1)); + await alice1.verifyElementNotPresent(new ConversationItem(alice1, testGroupName).build()); + }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1, charlie1); }); From d381058c0ee9df1558be813ce1f262fd3af79149 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 10:21:30 +1100 Subject: [PATCH 024/184] feat: add network target input to workflow --- .github/workflows/android-regression.yml | 96 ++++++++++++++---------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index c94225c07..9e23aa328 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -4,6 +4,15 @@ run-name: '${{ inputs.RISK }} regressions on ${{ github.head_ref || github.ref } on: workflow_dispatch: inputs: + NETWORK_TARGET: + description: 'network target to run the tests on' + required: true + type: choice + options: + - 'devnet' + - 'mainnet' + default: 'devnet' + APK_URL: description: 'apk url to test (.tar.xz)' required: true @@ -97,57 +106,62 @@ jobs: run: | pwd - # Check if devnet is accessible before choosing APK - echo "Checking devnet accessibility for APK selection..." - DEVNET_ACCESSIBLE=false - - # Retry logic - for attempt in 1 2 3; do - echo "Devnet check attempt $attempt/3..." - if curl -s --connect-timeout 5 --max-time 10 http://sesh-net.local:1280 >/dev/null 2>&1; then - echo "Devnet is accessible on attempt $attempt" - DEVNET_ACCESSIBLE=true - break - else - echo "Attempt $attempt failed" - if [ $attempt -lt 3 ]; then - echo "Waiting ${attempt}s before retry..." - sleep $attempt - fi - fi - done - - if [ "$DEVNET_ACCESSIBLE" = "false" ]; then - echo "Devnet is not accessible after 3 attempts" - fi + NETWORK_TARGET="${{ github.event.inputs.NETWORK_TARGET }}" + echo "Network target: $NETWORK_TARGET" - # Download and extract APK + # Download and extract APK wget -q -O session-android.apk.tar.xz ${{ github.event.inputs.APK_URL }} tar xf session-android.apk.tar.xz mv session-android-*universal extracted - # Choose APK based on devnet accessibility - if ls extracted/*automaticQa.apk 1>/dev/null 2>&1; then - if [ "$DEVNET_ACCESSIBLE" = "true" ]; then - echo "Using AQA build (devnet accessible)" + # Choose APK based on network target + if [ "$NETWORK_TARGET" = "devnet" ]; then + echo "Devnet target - checking devnet accessibility..." + DEVNET_ACCESSIBLE=false + + # Retry logic + for attempt in 1 2 3; do + echo "Devnet check attempt $attempt/3..." + if curl -s --connect-timeout 5 --max-time 10 http://sesh-net.local:1280 >/dev/null 2>&1; then + echo "Devnet is accessible on attempt $attempt" + DEVNET_ACCESSIBLE=true + break + else + echo "Attempt $attempt failed" + if [ $attempt -lt 3 ]; then + echo "Waiting ${attempt}s before retry..." + sleep $attempt + fi + fi + done + + if [ "$DEVNET_ACCESSIBLE" = "false" ]; then + echo "ERROR: Devnet is not accessible after 3 attempts, but devnet target was selected" + exit 1 + fi + + # Use AQA build for devnet + if ls extracted/*automaticQa.apk 1>/dev/null 2>&1; then + echo "Using AQA build for devnet" mv extracted/*automaticQa.apk extracted/session-android.apk echo "IS_AUTOMATIC_QA=true" >> $GITHUB_ENV else - echo "AQA build available but devnet not accessible - falling back to regular QA build" - if ls extracted/*qa.apk 1>/dev/null 2>&1; then - mv extracted/*qa.apk extracted/session-android.apk - echo "IS_AUTOMATIC_QA=false" >> $GITHUB_ENV - else - echo "No regular QA build found as fallback" - exit 1 - fi + echo "ERROR: No AQA build found for devnet target" + exit 1 fi - elif ls extracted/*qa.apk 1>/dev/null 2>&1; then - echo "Using regular QA build" - mv extracted/*qa.apk extracted/session-android.apk - echo "IS_AUTOMATIC_QA=false" >> $GITHUB_ENV + + elif [ "$NETWORK_TARGET" = "mainnet" ]; then + echo "Mainnet target - using regular QA build" + if ls extracted/*qa.apk 1>/dev/null 2>&1; then + mv extracted/*qa.apk extracted/session-android.apk + echo "IS_AUTOMATIC_QA=false" >> $GITHUB_ENV + else + echo "ERROR: No regular QA build found for mainnet target" + exit 1 + fi + else - echo "No suitable APK found" + echo "ERROR: Unknown network target: $NETWORK_TARGET" exit 1 fi From 82eee5210442dd341bd7eddb81fcba9cf019a0f4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 10:44:39 +1100 Subject: [PATCH 025/184] feat: write network target to allure report --- .github/workflows/android-regression.yml | 1 + .github/workflows/ios-regression.yml | 2 ++ github/actions/generate-publish-test-report/action.yml | 10 +++++++--- run/test/specs/utils/allure/allureHelpers.ts | 4 ++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 9e23aa328..489356d10 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -251,6 +251,7 @@ jobs: RISK: ${{github.event.inputs.RISK}} GITHUB_RUN_NUMBER: ${{ github.run_number }} GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }} + NETWORK_TARGET: ${{ github.event.inputs.NETWORK_TARGET }} - name: Upload results of this run uses: ./github/actions/upload-test-results diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index cee9624c2..69015b498 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -78,6 +78,7 @@ jobs: APPIUM_ADB_FULL_PATH: '' ANDROID_SDK_ROOT: '' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} + NETWORK_TARGET: 'mainnet' # iOS only supports mainnet for now _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) @@ -152,6 +153,7 @@ jobs: RISK: ${{github.event.inputs.RISK}} GITHUB_RUN_NUMBER: ${{ github.run_number }} GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }} + NETWORK_TARGET: ${{ env.NETWORK_TARGET }} - name: Upload results of this run uses: ./github/actions/upload-test-results diff --git a/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml index f23f1379b..aca9fafa4 100644 --- a/github/actions/generate-publish-test-report/action.yml +++ b/github/actions/generate-publish-test-report/action.yml @@ -15,6 +15,8 @@ inputs: required: true GITHUB_RUN_ATTEMPT: required: true + NETWORK_TARGET: + required: true runs: using: 'composite' @@ -27,10 +29,11 @@ runs: PLATFORM: ${{ inputs.PLATFORM }} BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }} GH_TOKEN: ${{ inputs.GH_TOKEN }} - APK_URL: ${{inputs.APK_URL}} - RISK: ${{inputs.RISK}} + APK_URL: ${{ inputs.APK_URL}} + RISK: ${{ inputs.RISK}} GITHUB_RUN_NUMBER: ${{ inputs.GITHUB_RUN_NUMBER}} GITHUB_RUN_ATTEMPT: ${{ inputs.GITHUB_RUN_ATTEMPT}} + NETWORK_TARGET: ${{ inputs.NETWORK_TARGET }} - name: Publish report to GitHub Pages if: ${{ success() }} @@ -42,9 +45,10 @@ runs: PLATFORM: ${{ inputs.PLATFORM }} BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }} GH_TOKEN: ${{ inputs.GH_TOKEN }} - RISK: ${{inputs.RISK}} + RISK: ${{ inputs.RISK}} GITHUB_RUN_NUMBER: ${{ inputs.GITHUB_RUN_NUMBER}} GITHUB_RUN_ATTEMPT: ${{ inputs.GITHUB_RUN_ATTEMPT}} + NETWORK_TARGET: ${{ inputs.NETWORK_TARGET }} - name: Annotate run summary with report link if: ${{ success() }} diff --git a/run/test/specs/utils/allure/allureHelpers.ts b/run/test/specs/utils/allure/allureHelpers.ts index eeabca5ad..27a343f1b 100644 --- a/run/test/specs/utils/allure/allureHelpers.ts +++ b/run/test/specs/utils/allure/allureHelpers.ts @@ -17,6 +17,7 @@ export interface ReportContext { build: string; artifact: string; risk: string; + networkTarget: string; runNumber: number; runAttempt: number; runID: number; @@ -33,6 +34,7 @@ export function getReportContextFromEnv(): ReportContext { const build = process.env.BUILD_NUMBER; const artifact = process.env.APK_URL; const risk = process.env.RISK?.trim() || 'full'; + const networkTarget = process.env.NETWORK_TARGET || 'mainnet'; // Default to mainnet for iOS const runNumber = Number(process.env.GITHUB_RUN_NUMBER); const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT); const runID = Number(process.env.GITHUB_RUN_ID); @@ -64,6 +66,7 @@ export function getReportContextFromEnv(): ReportContext { build, artifact, risk, + networkTarget, runNumber, runAttempt, runID, @@ -79,6 +82,7 @@ export async function writeEnvironmentProperties(ctx: ReportContext) { `platform=${ctx.platform}`, `build=${ctx.build}`, `artifact=${ctx.artifact}`, + `network=${ctx.networkTarget}`, `appium=https://github.com/session-foundation/session-appium/commit/${getGitCommitSha()}`, `branch=${getGitBranch()}`, ].join('\n'); From 02c4ea88d2405126d88df1e224a0423ef728e78b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 12:08:34 +1100 Subject: [PATCH 026/184] chore: update strings --- README.md | 2 +- run/localizer/lib | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a90028fc3..a57339500 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ nvm install nvm use git lfs install git lfs pull -git submodule update --init --recursive +git submodule update --init --recursive --remote pnpm install --frozen-lockfile ``` diff --git a/run/localizer/lib b/run/localizer/lib index bb08513f6..c0714a891 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit bb08513f637e7f0fb4de2675b5682a1e129ff4e0 +Subproject commit c0714a8916a38672584323e6084e8cedc36d7243 From fe2bf00a25016e48be8334b69499c0d3e82e46d0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 12:08:55 +1100 Subject: [PATCH 027/184] chore: update strings --- run/localizer/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/localizer/lib b/run/localizer/lib index bb08513f6..c0714a891 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit bb08513f637e7f0fb4de2675b5682a1e129ff4e0 +Subproject commit c0714a8916a38672584323e6084e8cedc36d7243 From 16001601d06c7c015f8d4910a63166fb8c725e8b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 14:44:05 +1100 Subject: [PATCH 028/184] fix: address broken tests --- run/screenshots/android/settings.png | 4 ++-- .../disappearing_community_invite.spec.ts | 2 +- .../specs/group_tests_delete_group.spec.ts | 3 +++ run/test/specs/locators/global.ts | 4 ++-- run/test/specs/locators/settings.ts | 5 +++-- run/test/specs/message_length.spec.ts | 21 +++++++++++++++++-- run/types/DeviceWrapper.ts | 4 ++-- 7 files changed, 32 insertions(+), 11 deletions(-) diff --git a/run/screenshots/android/settings.png b/run/screenshots/android/settings.png index e4bbcc15f..2c2885f24 100644 --- a/run/screenshots/android/settings.png +++ b/run/screenshots/android/settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e290aecdb8ba4e13606c65c3cf549b381c20abcdb3dbbfb882c7bceb328b41d0 -size 134845 +oid sha256:acbb60f750ef4b52e222cebddda610eae9ae143e38617a4f318286a6f7f3956d +size 139070 diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 8bf16c480..0843fb3d2 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -68,7 +68,7 @@ async function disappearingCommunityInviteMessage( // Leave Invite Contacts, Conversation Settings, Community, and open convo with Bob await alice1.navigateBack(); await alice1.navigateBack(); - await alice1.navigateBack(); + await alice1.onIOS().navigateBack(); // Android only needs to go back twice await alice1.clickOnElementAll(new ConversationItem(alice1, bob.userName)); // At this point the invite should have disappeared already so we just check it's not there await alice1.verifyElementNotPresent(new CommunityInvitation(alice1)); diff --git a/run/test/specs/group_tests_delete_group.spec.ts b/run/test/specs/group_tests_delete_group.spec.ts index 55d3fb6bd..96fbf606e 100644 --- a/run/test/specs/group_tests_delete_group.spec.ts +++ b/run/test/specs/group_tests_delete_group.spec.ts @@ -20,6 +20,9 @@ bothPlatformsIt({ }, allureDescription: `Verifies that an admin can delete a group successfully via the UI. The group members see the empty state control message, and the admin's conversation disappears from the home screen, even on a linked device.`, + allureLinks: { + android: 'SES-4883', + }, }); async function deleteGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/locators/global.ts b/run/test/specs/locators/global.ts index ae7e2707b..53cd31175 100644 --- a/run/test/specs/locators/global.ts +++ b/run/test/specs/locators/global.ts @@ -173,8 +173,8 @@ export class CTAFeature extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: `cta-feature-${this.index}`, + strategy: '-android uiautomator', + selector: `new UiSelector().resourceId("cta-feature-${this.index}").childSelector(new UiSelector().className("android.widget.TextView"))`, } as const; case 'ios': throw new Error('CTAFeature locator is not available on iOS'); diff --git a/run/test/specs/locators/settings.ts b/run/test/specs/locators/settings.ts index f4264c988..2e5ba6aae 100644 --- a/run/test/specs/locators/settings.ts +++ b/run/test/specs/locators/settings.ts @@ -158,8 +158,9 @@ export class NotificationsMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Notifications', + strategy: '-android uiautomator', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Notifications"))', } as const; case 'ios': return { diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index e2c1fea4f..1997dade3 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -11,6 +11,7 @@ import { MessageLengthOkayButton, SendButton, } from './locators/conversation'; +import { CTAButtonNegative } from './locators/global'; import { PlusButton } from './locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from './locators/start_conversation'; import { newUser } from './utils/create_account'; @@ -84,8 +85,8 @@ for (const testCase of messageLengthTestCases) { // Is the message short enough to send? if (testCase.shouldSend) { await device.waitForTextElementToBePresent(new MessageBody(device, message)); - } else { - // Modal appears, verify and dismiss + } else if (platform === 'ios') { + // iOS: Modal appears, verify and dismiss await device.checkModalStrings( englishStrippedStr('modalMessageTooLongTitle').toString(), englishStrippedStr('modalMessageTooLongDescription') @@ -94,6 +95,22 @@ for (const testCase of messageLengthTestCases) { ); await device.clickOnElementAll(new MessageLengthOkayButton(device)); await device.verifyElementNotPresent(new MessageBody(device, message)); + } else { + // Android: CTA appears, verify and dismiss + // Post-Pro is active on debug/qa builds by default + // This will be the default for both platforms once Pro is live + await device.checkCTAStrings( + englishStrippedStr('upgradeTo').toString(), + englishStrippedStr('proCallToActionLongerMessages').toString(), + [englishStrippedStr('theContinue').toString(), englishStrippedStr('cancel').toString()], + [ + englishStrippedStr('proFeatureListLongerMessages').toString(), + englishStrippedStr('proFeatureListPinnedConversations').toString(), + englishStrippedStr('proFeatureListLoadsMore').toString(), + ] + ); + await device.clickOnElementAll(new CTAButtonNegative(device)); + await device.verifyElementNotPresent(new MessageBody(device, message)); } }); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 96a9dfc36..de7b09b51 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2568,10 +2568,10 @@ export class DeviceWrapper { // Check features if expected if (features) { for (let i = 0; i < features.length; i++) { - const featureLocator = new CTAFeature(this, i + 1); + const featureLocator = new CTAFeature(this, i); const elFeature = await this.waitForTextElementToBePresent(featureLocator); const actualFeature = await this.getTextFromElement(elFeature); - this.assertTextMatches(actualFeature, features[i], `CTA feature ${i + 1}`); + this.assertTextMatches(actualFeature, features[i], `CTA feature ${i}`); } } From 371df50ad3e9d60719d3dbe208b1ed1e8241575f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 15:21:03 +1100 Subject: [PATCH 029/184] fix: try ipv4 first when installing deps --- github/actions/setup/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index 46d20d6f2..eb1c29e5e 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,6 +15,8 @@ runs: - name: Install test dependencies shell: bash + env: + NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls git status From 132aeaec212a534772c4ad4bfbd45f63cb83b267 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 15:27:29 +1100 Subject: [PATCH 030/184] chore: bump timeouts --- .npmrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 8b6171f0b..e8e52af3b 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ -@session-foundation:registry=https://verdaccio.oxen.rocks \ No newline at end of file +@session-foundation:registry=https://verdaccio.oxen.rocks +fetch-timeout=120000 +network-timeout=120000 \ No newline at end of file From 0c5995f042553c1ad5948a4197b968645fbeea95 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 15:39:09 +1100 Subject: [PATCH 031/184] chore: linting --- .npmrc | 1 + github/actions/setup/action.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index e8e52af3b..9f56a090c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,4 @@ @session-foundation:registry=https://verdaccio.oxen.rocks +# Verdaccio downloads were timing out on CI - increase timeouts fetch-timeout=120000 network-timeout=120000 \ No newline at end of file diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index eb1c29e5e..c0245021a 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,7 +15,7 @@ runs: - name: Install test dependencies shell: bash - env: + env: NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls From 3c40b21121056c811227b292a6fa22bac5fd690d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Feb 2026 16:48:19 +1100 Subject: [PATCH 032/184] feat: add multi admin promotion test --- run/test/specs/group_tests_promote.spec.ts | 109 ++++++++++++++++++++- run/test/specs/utils/get_account_id.ts | 11 ++- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index 88be01275..a74ca32ab 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -14,13 +14,14 @@ import { PromoteMembersMenuItem, } from './locators/groups'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { sortByPubkey } from './utils/get_account_id'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; androidIt({ - title: 'Promote to admin', + title: 'Promote to admin (one member)', risk: 'medium', - testCb: promoteToAdmin, + testCb: promoteSoloToAdmin, countOfDevicesNeeded: 3, allureSuites: { parent: 'Groups', @@ -29,11 +30,23 @@ androidIt({ allureDescription: 'Verifies that a group member can be promoted to Admin.', }); +androidIt({ + title: 'Promote to admin (multiple members)', + risk: 'medium', + testCb: promoteMultiToAdmin, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: 'Verifies that multiple members can be promoted to Admin in one action.', +}); + // The newly promoted admin will set disappearing messages to verify they have admin powers const time = DISAPPEARING_TIMES.ONE_MINUTE; const timerType = 'Disappear after send option'; -async function promoteToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { +async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Test group'; const { devices: { alice1, bob1, charlie1 }, @@ -94,7 +107,95 @@ async function promoteToAdmin(platform: SupportedPlatformsType, testInfo: TestIn device.waitForControlMessageToBePresent( englishStrippedStr('disappearingMessagesSet') .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) - .toString() + .toString(), + 30_000 + ) + ) + ); + await bob1.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: 'sent' }) + .toString() + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1, charlie1); + }); +} + +async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Test group'; + const { + devices: { alice1, bob1, charlie1 }, + prebuilt: { alice, bob, charlie }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + }); + const [firstUser, secondUser] = sortByPubkey(bob, charlie); + await test.step(`${alice.userName} promotes ${bob.userName} and ${charlie.userName}`, async () => { + // Navigate to Promote Members screen + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await alice1.clickOnElementAll(new PromoteMembersMenuItem(alice1)); + await alice1.clickOnElementAll(new Contact(alice1, 'Bob')); + await alice1.clickOnElementAll(new Contact(alice1, 'Charlie')); + await alice1.clickOnElementAll(new PromoteMemberFooterButton(alice1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Promote'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('promote').toString(), + englishStrippedStr('adminPromoteTwoDescription') + .withArgs({ name: firstUser, other_name: secondUser }) + .toString() + ); + // This is a string that's part of the modal but not part of the modal description element + await alice1.waitForTextElementToBePresent({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('promoteAdminsWarning').toString()}")`, + }); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Confirm Promotion'), async () => { + await alice1.clickOnElementAll(new PromoteMemberModalConfirm(alice1)); + await alice1.checkModalStrings( + englishStrippedStr('confirmPromotion').toString(), + englishStrippedStr('confirmPromotionDescription').toString() + ); + }); + await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); + }); + await alice1.navigateBack(); + await alice1.navigateBack(); + await test.step('Verify every member sees the promotion control message', async () => { + await Promise.all([ + alice1.waitForControlMessageToBePresent( + englishStrippedStr('adminTwoPromotedToAdmin') + .withArgs({ name: firstUser, other_name: secondUser }) + .toString() + ), + bob1.waitForControlMessageToBePresent( + englishStrippedStr('groupPromotedYouTwo') + .withArgs({ other_name: charlie.userName }) + .toString() + ), + charlie1.waitForControlMessageToBePresent( + englishStrippedStr('groupPromotedYouTwo').withArgs({ other_name: bob.userName }).toString() + ), + ]); + }); + await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { + // Check to see if Bob has admin powers by setting disappearing messages + await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await Promise.all( + [alice1, charlie1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) + .toString(), + 30_000 ) ) ); diff --git a/run/test/specs/utils/get_account_id.ts b/run/test/specs/utils/get_account_id.ts index 4820217e2..0226e8648 100644 --- a/run/test/specs/utils/get_account_id.ts +++ b/run/test/specs/utils/get_account_id.ts @@ -1,9 +1,16 @@ +import { type StateUser } from '@session-foundation/qa-seeder'; + import { User } from '../../../types/testing'; import { SupportedPlatformsType } from './open_app'; -export function sortByPubkey(...users: Array) { +// Sorts users by pubkey hex (StateUser.sessionId from qa-seeder or User.accountID from local types) and returns usernames +export function sortByPubkey(...users: Array) { return [...users] - .sort((a, b) => a.accountID.localeCompare(b.accountID)) + .sort((a, b) => { + const aKey = 'accountID' in a ? a.accountID : String(a.sessionId); + const bKey = 'accountID' in b ? b.accountID : String(b.sessionId); + return aKey.localeCompare(bKey); + }) .map(user => user.userName); } From db767ba626ce25b665aed5b35e0d6f6457cf992b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 12:03:10 +1100 Subject: [PATCH 033/184] fix: re-add disabling animation on ios --- run/test/specs/utils/capabilities_ios.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index a4e7fce48..df370d819 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -34,6 +34,7 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { env: { debugDisappearingMessageDurations: 'true', communityPollLimit: '3', + animationsEnabled: 'false', }, }, } as AppiumXCUITestCapabilities; From 7e0bfcb4f19e6430483fa565e7a9624988b9107b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 13:54:29 +1100 Subject: [PATCH 034/184] feat: add linked device promote test --- run/test/specs/group_tests_promote.spec.ts | 186 +++++++++++++++++- .../specs/linked_device_restore_group.spec.ts | 2 +- run/test/specs/utils/restore_account.ts | 2 + run/types/DeviceWrapper.ts | 25 ++- 4 files changed, 199 insertions(+), 16 deletions(-) diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index a74ca32ab..0442056cb 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -3,19 +3,24 @@ import { test, type TestInfo } from '@playwright/test'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; -import { DISAPPEARING_TIMES } from '../../types/testing'; +import { DISAPPEARING_TIMES, USERNAME } from '../../types/testing'; import { ConversationSettings } from './locators/conversation'; import { Contact } from './locators/global'; import { ConfirmPromotionModalButton, ManageAdminsMenuItem, + MemberStatus, PromoteMemberFooterButton, PromoteMemberModalConfirm, PromoteMembersMenuItem, } from './locators/groups'; +import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_Charlie1_friends_group } from './state_builder'; +import { newUser } from './utils/create_account'; +import { createGroup } from './utils/create_group'; import { sortByPubkey } from './utils/get_account_id'; -import { closeApp, SupportedPlatformsType } from './utils/open_app'; +import { closeApp, openAppFourDevices, SupportedPlatformsType } from './utils/open_app'; +import { restoreAccount } from './utils/restore_account'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; androidIt({ @@ -30,6 +35,19 @@ androidIt({ allureDescription: 'Verifies that a group member can be promoted to Admin.', }); +androidIt({ + title: 'Promote to admin (linked device)', + risk: 'medium', + testCb: promoteSoloLinked, + countOfDevicesNeeded: 4, + allureSuites: { + parent: 'Groups', + suite: 'Edit Group', + }, + allureDescription: + 'Verifies that a previously promoted admin has admin powers on their linked device.', +}); + androidIt({ title: 'Promote to admin (multiple members)', risk: 'medium', @@ -85,6 +103,10 @@ async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: Te ); }); await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); + // This is not tied to Bob but they're the only admin this status can apply to + await alice1.waitForTextElementToBePresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ); }); await alice1.navigateBack(); await alice1.navigateBack(); @@ -99,6 +121,19 @@ async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: Te ); await bob1.waitForControlMessageToBePresent(englishStrippedStr('groupPromotedYou').toString()); }); + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await Promise.all([ + alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)), + alice1.verifyElementNotPresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ), + alice1.verifyElementNotPresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionFailed').toString()) + ), + ]); + await alice1.navigateBack(); + await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); @@ -123,6 +158,102 @@ async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: Te }); } +async function promoteSoloLinked(platform: SupportedPlatformsType, testInfo: TestInfo) { + const testGroupName = 'Test group'; + const { device1, device2, device3, device4 } = await openAppFourDevices(platform, testInfo); + const [alice, bob, charlie] = await Promise.all([ + newUser(device1, USERNAME.ALICE), + newUser(device2, USERNAME.BOB), + newUser(device3, USERNAME.CHARLIE), + ]); + await createGroup(platform, device1, alice, device2, bob, device3, charlie, testGroupName); + await test.step(`${alice.userName} promotes ${bob.userName}`, async () => { + // Navigate to Promote Members screen + await device1.clickOnElementAll(new ConversationSettings(device1)); + await device1.clickOnElementAll(new ManageAdminsMenuItem(device1)); + await device1.clickOnElementAll(new PromoteMembersMenuItem(device1)); + await device1.clickOnElementAll(new Contact(device1, bob.userName)); + await device1.clickOnElementAll(new PromoteMemberFooterButton(device1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Promote'), async () => { + await device1.checkModalStrings( + englishStrippedStr('promote').toString(), + englishStrippedStr('adminPromoteDescription').withArgs({ name: bob.userName }).toString() + ); + // This is a string that's part of the modal but not part of the modal description element + await device1.waitForTextElementToBePresent({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("${englishStrippedStr('promoteAdminsWarning').toString()}")`, + }); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Confirm Promotion'), async () => { + await device1.clickOnElementAll(new PromoteMemberModalConfirm(device1)); + await device1.checkModalStrings( + englishStrippedStr('confirmPromotion').toString(), + englishStrippedStr('confirmPromotionDescription').toString() + ); + }); + await device1.clickOnElementAll(new ConfirmPromotionModalButton(device1)); + // This is not tied to Bob but they're the only admin this status can apply to + await device1.waitForTextElementToBePresent( + new MemberStatus(device1).build(englishStrippedStr('adminPromotionSent').toString()) + ); + }); + await device1.navigateBack(); + await device1.navigateBack(); + await test.step('Verify every member sees the promotion control message', async () => { + await Promise.all( + [device1, device3].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('adminPromotedToAdmin').withArgs({ name: bob.userName }).toString(), + 30_000 + ) + ) + ); + await device2.waitForControlMessageToBePresent( + englishStrippedStr('groupPromotedYou').toString() + ); + }); + await device1.clickOnElementAll(new ConversationSettings(device1)); + await device1.clickOnElementAll(new ManageAdminsMenuItem(device1)); + await Promise.all([ + device1.waitForTextElementToBePresent(new Contact(device1, bob.userName)), + device1.verifyElementNotPresent( + new MemberStatus(device1).build(englishStrippedStr('adminPromotionSent').toString()) + ), + device1.verifyElementNotPresent( + new MemberStatus(device1).build(englishStrippedStr('adminPromotionFailed').toString()) + ), + ]); + await device1.navigateBack(); + await device1.navigateBack(); + await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { + // Check to see if Bob has admin powers by setting disappearing messages + await setDisappearingMessage(platform, device2, ['Group', timerType, time]); + await Promise.all( + [device1, device3].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSet') + .withArgs({ name: bob.userName, time, disappearing_messages_type: 'sent' }) + .toString(), + 30_000 + ) + ) + ); + await device2.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time, disappearing_messages_type: 'sent' }) + .toString() + ); + }); + await restoreAccount(device4, bob, 'bob2'); + await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); + await device4.clickOnElementAll(new ConversationSettings(device4)); + await device4.clickOnElementAll(new ManageAdminsMenuItem(device4)); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device1, device2, device3, device4); + }); +} + async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Test group'; const { @@ -166,6 +297,10 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T ); }); await alice1.clickOnElementAll(new ConfirmPromotionModalButton(alice1)); + // This is not tied to Bob/Charlie but they're the only admin this status can apply to + await alice1.waitForTextElementToBePresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ); }); await alice1.navigateBack(); await alice1.navigateBack(); @@ -174,18 +309,35 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T alice1.waitForControlMessageToBePresent( englishStrippedStr('adminTwoPromotedToAdmin') .withArgs({ name: firstUser, other_name: secondUser }) - .toString() + .toString(), + 10_000 ), bob1.waitForControlMessageToBePresent( englishStrippedStr('groupPromotedYouTwo') .withArgs({ other_name: charlie.userName }) - .toString() + .toString(), + 45_000 ), charlie1.waitForControlMessageToBePresent( - englishStrippedStr('groupPromotedYouTwo').withArgs({ other_name: bob.userName }).toString() + englishStrippedStr('groupPromotedYouTwo').withArgs({ other_name: bob.userName }).toString(), + 45_000 ), ]); }); + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new ManageAdminsMenuItem(alice1)); + await Promise.all([ + alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)), + alice1.waitForTextElementToBePresent(new Contact(alice1, charlie.userName)), + alice1.verifyElementNotPresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) + ), + alice1.verifyElementNotPresent( + new MemberStatus(alice1).build(englishStrippedStr('adminPromotionFailed').toString()) + ), + ]); + await alice1.navigateBack(); + await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); @@ -205,6 +357,30 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T .toString() ); }); + await test.step(`Verify ${charlie.userName} has admin powers by setting disappearing messages`, async () => { + // Check to see if Bob has admin powers by setting disappearing messages + const charlieTime = DISAPPEARING_TIMES.TWELVE_HOURS; + await setDisappearingMessage(platform, charlie1, ['Group', timerType, charlieTime]); + await Promise.all( + [alice1, bob1].map(device => + device.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSet') + .withArgs({ + name: charlie.userName, + time: charlieTime, + disappearing_messages_type: 'sent', + }) + .toString(), + 30_000 + ) + ) + ); + await charlie1.waitForControlMessageToBePresent( + englishStrippedStr('disappearingMessagesSetYou') + .withArgs({ time: charlieTime, disappearing_messages_type: 'sent' }) + .toString() + ); + }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1, charlie1); }); diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 289e53d83..54a14ba3c 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -34,7 +34,7 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo const aliceMessage = `${USERNAME.ALICE} to ${testGroupName}`; const bobMessage = `${USERNAME.BOB} to ${testGroupName}`; const charlieMessage = `${USERNAME.CHARLIE} to ${testGroupName}`; - await restoreAccount(device4, alice); + await restoreAccount(device4, alice, 'alice2'); // Check that group has loaded on linked device await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index cb496910d..2ae005a8c 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -15,9 +15,11 @@ import { handleNotificationPermissions } from './permissions'; export const restoreAccount = async ( device: DeviceWrapper, user: User, + deviceIdentity: string, options?: BaseSetupOptions ) => { const { allowNotificationPermissions = false } = options || {}; + device.setDeviceIdentity(deviceIdentity); await device.clickOnElementAll(new AccountRestoreButton(device)); await device.inputText(user.recoveryPhrase, new SeedPhraseInput(device)); // Wait for continue button to become active diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 8475796e2..f10ec2e4c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1230,19 +1230,24 @@ export class DeviceWrapper { if (element) { // Elements can disappear in the GUI but still be present in the DOM + let isVisible: boolean; try { - const isVisible = await this.isVisible(element.ELEMENT); - if (isVisible) { - throw new Error( - `Element with ${description} is visible after ${maxWait}ms when it should not be` - ); - } - // Element exists but not visible - that's okay - this.log(`Element with ${description} exists but is not visible`); + isVisible = await this.isVisible(element.ELEMENT); } catch (e) { - // Stale element or other error - element is gone, that's okay - this.log(`Element with ${description} is not present (stale reference)`); + // Stale reference or other error checking visibility + const errorMsg = e instanceof Error ? e.message : String(e); + throw new Error( + `Element with ${description} has stale reference or error checking visibility: ${errorMsg}` + ); + } + + if (isVisible) { + throw new Error( + `Element with ${description} is visible after ${maxWait}ms when it should not be` + ); } + // Element exists but not visible - that's okay + this.log(`Element with ${description} exists but is not visible`); } else { this.log(`Verified no element with ${description} is present`); } From bf60752120ebd424b3881573872a129beebaadb6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 15:33:57 +1100 Subject: [PATCH 035/184] fix: use relative heights for scrollUp and scrollDown --- run/types/DeviceWrapper.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index bc91679c3..8f47c13b4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2259,11 +2259,13 @@ export class DeviceWrapper { this.info('Swiped left on ', selector); } + + // Swipe horizontally from 20% to 80% of screen width at the vertical center public async swipeRight() { const { width, height } = await this.getWindowRect(); - // Swipe horizontally from 20% to 80% of screen width at the vertical center await this.scroll({ x: width * 0.2, y: height / 2 }, { x: width * 0.8, y: height / 2 }, 100); } + public async swipeLeft(accessibilityId: AccessibilityId, text: string) { const el = await this.findMatchingTextAndAccessibilityId(accessibilityId, text); @@ -2283,14 +2285,19 @@ export class DeviceWrapper { // let some time for swipe action to happen and UI to update } + // Swipe vertically from 70% to 30% of screen height at the horizontal center public async scrollDown() { - await this.scroll({ x: 760, y: 1500 }, { x: 760, y: 710 }, 100); + const { width, height } = await this.getWindowRect(); + await this.scroll({ x: width / 2, y: height * 0.7 }, { x: width / 2, y: height * 0.3 }, 100); } + // Swipe vertically from 30% to 70% of screen height at the horizontal center public async scrollUp() { - await this.scroll({ x: 760, y: 710 }, { x: 760, y: 1500 }, 100); + const { width, height } = await this.getWindowRect(); + await this.scroll({ x: width / 2, y: height * 0.3 }, { x: width / 2, y: height * 0.7 }, 100); } + // Swipe vertically from 95% to 35% of screen height at the horizontal center public async swipeFromBottom(): Promise { const { width, height } = await this.getWindowRect(); From 20e22014d78dfb8938c799e64d7e002f960e7c34 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 15:34:47 +1100 Subject: [PATCH 036/184] fix: use username locator for group info on ios --- run/test/specs/locators/groups.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 032bba16f..a4cc3cbce 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -366,7 +366,8 @@ export class UpdateGroupInformation extends LocatorsInterface { } return { strategy: 'accessibility id', - selector: groupName, + selector: 'Username', + text: groupName, }; } } From d9f8c7d382e016f4d2d9eb28720ebcb6960f5e2f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 15:35:47 +1100 Subject: [PATCH 037/184] chore: bump timeouts --- .npmrc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 8b6171f0b..9f56a090c 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,4 @@ -@session-foundation:registry=https://verdaccio.oxen.rocks \ No newline at end of file +@session-foundation:registry=https://verdaccio.oxen.rocks +# Verdaccio downloads were timing out on CI - increase timeouts +fetch-timeout=120000 +network-timeout=120000 \ No newline at end of file From 551d113b0185ea4c2b72101fc63ff416799073a3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 15:38:09 +1100 Subject: [PATCH 038/184] chore: resolve ipv4 first --- github/actions/setup/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index 46d20d6f2..eb1c29e5e 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,6 +15,8 @@ runs: - name: Install test dependencies shell: bash + env: + NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls git status From a1e67ac1ea5ee33ccaac9104f370a08d188cbff9 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 3 Feb 2026 15:40:44 +1100 Subject: [PATCH 039/184] chore: linting --- github/actions/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index eb1c29e5e..c0245021a 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,7 +15,7 @@ runs: - name: Install test dependencies shell: bash - env: + env: NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls From d538cf52885082d2221daf6176298692bf6f06c4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 4 Feb 2026 07:42:56 +1100 Subject: [PATCH 040/184] Tweaked the locator for group name on iOS --- run/test/specs/locators/groups.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 032bba16f..49cdce32d 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -366,7 +366,8 @@ export class UpdateGroupInformation extends LocatorsInterface { } return { strategy: 'accessibility id', - selector: groupName, + selector: 'Username', + text: groupName }; } } From 63dfc57ee13d07b0173d2cc1849225c942d08cad Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 4 Feb 2026 07:44:59 +1100 Subject: [PATCH 041/184] Ran the linter --- run/test/specs/locators/groups.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/locators/groups.ts b/run/test/specs/locators/groups.ts index 49cdce32d..a4cc3cbce 100644 --- a/run/test/specs/locators/groups.ts +++ b/run/test/specs/locators/groups.ts @@ -367,7 +367,7 @@ export class UpdateGroupInformation extends LocatorsInterface { return { strategy: 'accessibility id', selector: 'Username', - text: groupName + text: groupName, }; } } From 45162ee7bc30284426e1bb45c2c4f85a59fa1e38 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 4 Feb 2026 10:37:49 +1100 Subject: [PATCH 042/184] chore: fetch qa-seeder from npm again --- .npmrc | 4 ---- package.json | 3 +-- pnpm-lock.yaml | 48 ++++++++++++++++++++++++------------------------ 3 files changed, 25 insertions(+), 30 deletions(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 9f56a090c..000000000 --- a/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -@session-foundation:registry=https://verdaccio.oxen.rocks -# Verdaccio downloads were timing out on CI - increase timeouts -fetch-timeout=120000 -network-timeout=120000 \ No newline at end of file diff --git a/package.json b/package.json index c47e253cd..ffcd84f1c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "typescript-eslint": "^8.15.0", "undici": "^7.19.1", "uuid": "^13.0.0", - "undici": "^7.19.1", "wd": "^1.14.0", "wdio-wait-for": "^2.2.6" }, @@ -68,7 +67,7 @@ "dependencies": { "@playwright/test": "^1.45.1", "@session-foundation/playwright-reporter": "^0.0.8", - "@session-foundation/qa-seeder": "0.1.20", + "@session-foundation/qa-seeder": "0.1.25", "appium": "^2.4.1", "appium-uiautomator2-driver": "3.8.2", "appium-xcuitest-driver": "^7.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aee15b17..20cac2021 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^0.0.8 version: 0.0.8 '@session-foundation/qa-seeder': - specifier: 0.1.20 - version: 0.1.20 + specifier: 0.1.25 + version: 0.1.25 appium: specifier: ^2.4.1 version: 2.19.0 @@ -478,23 +478,23 @@ packages: engines: {node: '>=16.3.0'} hasBin: true - '@session-foundation/basic-types@0.0.4': - resolution: {integrity: sha512-EzLYOrE3+BLKi8l7jb1EXsOKNUthR9JA6RG/sTfYSHkal4PsqlekDjul5swh9Inokyzh8zZbJ3As2qRMwKpz4Q==} + '@session-foundation/basic-types@0.0.6': + resolution: {integrity: sha512-7+ztoNhirWfCeiPbUFTWplI9Crk65UAiye0dGoKf9TWBTobyUis5PB57ySxUTA0putWlHss366wCtHsX51uzHg==} - '@session-foundation/mnemonic@0.0.6': - resolution: {integrity: sha512-Q8Atgk3CVlExXJMRpqzK0ej2ZRP+uWoaoVCqPnDAaY8aS5v9ksD4BTriBm/DjXL/O9nWO6svf6++6HWXXjxCNQ==} + '@session-foundation/libsession-wasm@0.0.10': + resolution: {integrity: sha512-CdxsvIUVg000GVSrU4KN/qFjX+1syduUg23jxYEYIh59trFo3pKoyPfeUmnUjV40zJsFYevtyE/ybo/CpvHOZw==} + + '@session-foundation/mnemonic@0.0.8': + resolution: {integrity: sha512-8YCHnNDEiWdX46wy5knvXN/601J7Vs8mkqwf+4OCJP8ksnHz8CVZDpdwO/RVx1DwDx9fYZLd4OqugY/JexPdoQ==} '@session-foundation/playwright-reporter@0.0.8': resolution: {integrity: sha512-VvEWcl1JgQBqTdiSxhsL/RKZziuXHa+5uginn6F2LDAnKxidNUcB6SnCfXfyr+C0+9YQqQjmkBhYtxYJMJWcWw==} - '@session-foundation/qa-seeder@0.1.20': - resolution: {integrity: sha512-KJRlG9AcmT+JF1mV0fY6/fWECodba15QK9vweG2gBuH+O1J+vCoS67vh9YuBpi2to2bIYB9K62s/9UzhJa68PA==} - - '@session-foundation/session-tooling@0.1.1': - resolution: {integrity: sha512-R1r+KvdIkpFryPBhnJo0rPPh5fHUtQ3qlraskRbaxFiZ7vxx8bbxRq0VUJXB5ZrostI3KME1KN1kJdlHCAqnRw==} + '@session-foundation/qa-seeder@0.1.25': + resolution: {integrity: sha512-VSVrjLhZPt+l4luNNd38RilFmWRaWysRd0kAq7s+Zq+W6ZnUAAI7lU6WD82TsBWa+b545kzr8XyKryp4MuLwjw==} - '@session-foundation/sodium@0.0.3': - resolution: {integrity: sha512-Mh16FmpMn1G6038a+u1jOvAJA40t8lTIMoAhiKhN+x03t1Y80yWg3gniQaav2JhHfXUAqoJe4Ecogwn5XgYZGA==} + '@session-foundation/sodium@0.0.6': + resolution: {integrity: sha512-PDecdG3LR9rOinPHGVlTaJc1E0e2SS6ctfFhfgrEbEW3ZWymLXO5D36oAfNY1yuxM2QyiXCO+L99IPv4biGZgw==} '@sidvind/better-ajv-errors@3.0.1': resolution: {integrity: sha512-++1mEYIeozfnwWI9P1ECvOPoacy+CgDASrmGvXPMCcqgx0YUzB01vZ78uHdQ443V6sTY+e9MzHqmN9DOls02aw==} @@ -3951,11 +3951,13 @@ snapshots: - react-native-b4a - supports-color - '@session-foundation/basic-types@0.0.4': + '@session-foundation/basic-types@0.0.6': dependencies: buffer-crc32: 1.0.0 - '@session-foundation/mnemonic@0.0.6': + '@session-foundation/libsession-wasm@0.0.10': {} + + '@session-foundation/mnemonic@0.0.8': dependencies: buffer-crc32: 1.0.0 @@ -3963,19 +3965,17 @@ snapshots: dependencies: lodash: 4.17.23 - '@session-foundation/qa-seeder@0.1.20': + '@session-foundation/qa-seeder@0.1.25': dependencies: - '@session-foundation/basic-types': 0.0.4 - '@session-foundation/mnemonic': 0.0.6 - '@session-foundation/session-tooling': 0.1.1 - '@session-foundation/sodium': 0.0.3 + '@session-foundation/basic-types': 0.0.6 + '@session-foundation/libsession-wasm': 0.0.10 + '@session-foundation/mnemonic': 0.0.8 + '@session-foundation/sodium': 0.0.6 lodash: 4.17.23 - '@session-foundation/session-tooling@0.1.1': {} - - '@session-foundation/sodium@0.0.3': + '@session-foundation/sodium@0.0.6': dependencies: - '@session-foundation/basic-types': 0.0.4 + '@session-foundation/basic-types': 0.0.6 buffer-crc32: 1.0.0 libsodium-wrappers-sumo: 0.7.16 From 764814147d8d659d833845521a0fc109da95e56d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 4 Feb 2026 11:28:42 +1100 Subject: [PATCH 043/184] chore: remove local ipv4 hack --- github/actions/setup/action.yml | 2 -- package.json | 3 +-- pnpm-lock.yaml | 19 +++++-------------- run/test/specs/state_builder/index.ts | 6 ------ 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index c0245021a..46d20d6f2 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,8 +15,6 @@ runs: - name: Install test dependencies shell: bash - env: - NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls git status diff --git a/package.json b/package.json index ffcd84f1c..c5429be57 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.15.0", - "undici": "^7.19.1", "uuid": "^13.0.0", "wd": "^1.14.0", "wdio-wait-for": "^2.2.6" @@ -67,7 +66,7 @@ "dependencies": { "@playwright/test": "^1.45.1", "@session-foundation/playwright-reporter": "^0.0.8", - "@session-foundation/qa-seeder": "0.1.25", + "@session-foundation/qa-seeder": "^0.1.26", "appium": "^2.4.1", "appium-uiautomator2-driver": "3.8.2", "appium-xcuitest-driver": "^7.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20cac2021..cc62f8821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^0.0.8 version: 0.0.8 '@session-foundation/qa-seeder': - specifier: 0.1.25 - version: 0.1.25 + specifier: ^0.1.26 + version: 0.1.26 appium: specifier: ^2.4.1 version: 2.19.0 @@ -138,9 +138,6 @@ importers: typescript-eslint: specifier: ^8.15.0 version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) - undici: - specifier: ^7.19.1 - version: 7.20.0 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -490,8 +487,8 @@ packages: '@session-foundation/playwright-reporter@0.0.8': resolution: {integrity: sha512-VvEWcl1JgQBqTdiSxhsL/RKZziuXHa+5uginn6F2LDAnKxidNUcB6SnCfXfyr+C0+9YQqQjmkBhYtxYJMJWcWw==} - '@session-foundation/qa-seeder@0.1.25': - resolution: {integrity: sha512-VSVrjLhZPt+l4luNNd38RilFmWRaWysRd0kAq7s+Zq+W6ZnUAAI7lU6WD82TsBWa+b545kzr8XyKryp4MuLwjw==} + '@session-foundation/qa-seeder@0.1.26': + resolution: {integrity: sha512-VkwcDJPjaO79xYMZ/fc3aJxbYMKmNf7JxW7IoFfvhWL+1oI1D5FWC8SbC+rEFaxc7wU/bf8wR9QJZ1pHusXwIA==} '@session-foundation/sodium@0.0.6': resolution: {integrity: sha512-PDecdG3LR9rOinPHGVlTaJc1E0e2SS6ctfFhfgrEbEW3ZWymLXO5D36oAfNY1yuxM2QyiXCO+L99IPv4biGZgw==} @@ -3258,10 +3255,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.20.0: - resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} - engines: {node: '>=20.18.1'} - universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3965,7 +3958,7 @@ snapshots: dependencies: lodash: 4.17.23 - '@session-foundation/qa-seeder@0.1.25': + '@session-foundation/qa-seeder@0.1.26': dependencies: '@session-foundation/basic-types': 0.0.6 '@session-foundation/libsession-wasm': 0.0.10 @@ -7166,8 +7159,6 @@ snapshots: undici-types@7.16.0: {} - undici@7.20.0: {} - universalify@0.1.2: {} universalify@2.0.1: {} diff --git a/run/test/specs/state_builder/index.ts b/run/test/specs/state_builder/index.ts index 9944a20b5..36cfbd44a 100644 --- a/run/test/specs/state_builder/index.ts +++ b/run/test/specs/state_builder/index.ts @@ -1,9 +1,3 @@ -import { Agent, setGlobalDispatcher } from 'undici'; - -// Force IPv4 connections to work around Node.js fetch/undici lacking "Happy Eyeballs" (RFC 6555) -// https://github.com/node-fetch/node-fetch/issues/1297 -setGlobalDispatcher(new Agent({ connect: { family: 4 } })); - import type { TestInfo } from '@playwright/test'; import { From 8d0f11c7767c6bfc1e20f1d870a88cc6ac7f0a05 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 4 Feb 2026 11:31:11 +1100 Subject: [PATCH 044/184] Revert "chore: remove local ipv4 hack" This reverts commit 764814147d8d659d833845521a0fc109da95e56d. --- github/actions/setup/action.yml | 2 ++ package.json | 3 ++- pnpm-lock.yaml | 19 ++++++++++++++----- run/test/specs/state_builder/index.ts | 6 ++++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index 46d20d6f2..c0245021a 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -15,6 +15,8 @@ runs: - name: Install test dependencies shell: bash + env: + NODE_OPTIONS: '--dns-result-order=ipv4first' run: | ls git status diff --git a/package.json b/package.json index c5429be57..ffcd84f1c 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.15.0", + "undici": "^7.19.1", "uuid": "^13.0.0", "wd": "^1.14.0", "wdio-wait-for": "^2.2.6" @@ -66,7 +67,7 @@ "dependencies": { "@playwright/test": "^1.45.1", "@session-foundation/playwright-reporter": "^0.0.8", - "@session-foundation/qa-seeder": "^0.1.26", + "@session-foundation/qa-seeder": "0.1.25", "appium": "^2.4.1", "appium-uiautomator2-driver": "3.8.2", "appium-xcuitest-driver": "^7.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc62f8821..20cac2021 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^0.0.8 version: 0.0.8 '@session-foundation/qa-seeder': - specifier: ^0.1.26 - version: 0.1.26 + specifier: 0.1.25 + version: 0.1.25 appium: specifier: ^2.4.1 version: 2.19.0 @@ -138,6 +138,9 @@ importers: typescript-eslint: specifier: ^8.15.0 version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) + undici: + specifier: ^7.19.1 + version: 7.20.0 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -487,8 +490,8 @@ packages: '@session-foundation/playwright-reporter@0.0.8': resolution: {integrity: sha512-VvEWcl1JgQBqTdiSxhsL/RKZziuXHa+5uginn6F2LDAnKxidNUcB6SnCfXfyr+C0+9YQqQjmkBhYtxYJMJWcWw==} - '@session-foundation/qa-seeder@0.1.26': - resolution: {integrity: sha512-VkwcDJPjaO79xYMZ/fc3aJxbYMKmNf7JxW7IoFfvhWL+1oI1D5FWC8SbC+rEFaxc7wU/bf8wR9QJZ1pHusXwIA==} + '@session-foundation/qa-seeder@0.1.25': + resolution: {integrity: sha512-VSVrjLhZPt+l4luNNd38RilFmWRaWysRd0kAq7s+Zq+W6ZnUAAI7lU6WD82TsBWa+b545kzr8XyKryp4MuLwjw==} '@session-foundation/sodium@0.0.6': resolution: {integrity: sha512-PDecdG3LR9rOinPHGVlTaJc1E0e2SS6ctfFhfgrEbEW3ZWymLXO5D36oAfNY1yuxM2QyiXCO+L99IPv4biGZgw==} @@ -3255,6 +3258,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.20.0: + resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} + engines: {node: '>=20.18.1'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3958,7 +3965,7 @@ snapshots: dependencies: lodash: 4.17.23 - '@session-foundation/qa-seeder@0.1.26': + '@session-foundation/qa-seeder@0.1.25': dependencies: '@session-foundation/basic-types': 0.0.6 '@session-foundation/libsession-wasm': 0.0.10 @@ -7159,6 +7166,8 @@ snapshots: undici-types@7.16.0: {} + undici@7.20.0: {} + universalify@0.1.2: {} universalify@2.0.1: {} diff --git a/run/test/specs/state_builder/index.ts b/run/test/specs/state_builder/index.ts index 36cfbd44a..9944a20b5 100644 --- a/run/test/specs/state_builder/index.ts +++ b/run/test/specs/state_builder/index.ts @@ -1,3 +1,9 @@ +import { Agent, setGlobalDispatcher } from 'undici'; + +// Force IPv4 connections to work around Node.js fetch/undici lacking "Happy Eyeballs" (RFC 6555) +// https://github.com/node-fetch/node-fetch/issues/1297 +setGlobalDispatcher(new Agent({ connect: { family: 4 } })); + import type { TestInfo } from '@playwright/test'; import { From b075ad681eb1bf32392ba2487a3b4f1782b6c421 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 4 Feb 2026 11:50:04 +1100 Subject: [PATCH 045/184] fix: blind scroll to reveal Block on iOS --- run/test/specs/linked_device_block_user.spec.ts | 1 + run/test/specs/user_actions_block_conversation_options.spec.ts | 1 + run/test/specs/user_actions_unblock_user.spec.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index d288886ab..0dae1bf67 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -35,6 +35,7 @@ async function blockUserInConversationOptions( await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Select Block option await sleepFor(500); + await alice1.onIOS().scrollDown(); // Blind scroll because Block option is obscured by system UI on iOS await alice1.clickOnElementAll(new BlockUser(alice1)); await alice1.checkModalStrings( englishStrippedStr('block').toString(), 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 0c44cd58d..abed9178c 100644 --- a/run/test/specs/user_actions_block_conversation_options.spec.ts +++ b/run/test/specs/user_actions_block_conversation_options.spec.ts @@ -44,6 +44,7 @@ async function blockUserInConversationSettings( await alice1.clickOnElementAll(new ConversationSettings(alice1)); // Select Block option await sleepFor(500); + await alice1.onIOS().scrollDown(); // Blind scroll because Block option is obscured by system UI on iOS await alice1.clickOnElementAll(new BlockUser(alice1)); // Check modal strings await alice1.checkModalStrings( diff --git a/run/test/specs/user_actions_unblock_user.spec.ts b/run/test/specs/user_actions_unblock_user.spec.ts index 396420332..982184f36 100644 --- a/run/test/specs/user_actions_unblock_user.spec.ts +++ b/run/test/specs/user_actions_unblock_user.spec.ts @@ -30,6 +30,7 @@ async function unblockUser(platform: SupportedPlatformsType, testInfo: TestInfo) }); const blockedMessage = `Blocked message from ${bob.userName} to ${alice.userName}`; await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.onIOS().scrollDown(); // Blind scroll because Block option is obscured by system UI on iOS await alice1.clickOnElementAll(new BlockUser(alice1)); await alice1.checkModalStrings( englishStrippedStr('block').toString(), From a01a3eac2ae6ced506f837e1e547cd28d6d967c3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 4 Feb 2026 12:00:34 +1100 Subject: [PATCH 046/184] fix: sleep before interacting with UPM --- run/test/specs/community_requests_on.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index 1a7a53b89..cb81c9a11 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -68,6 +68,7 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo }); await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { await device1.clickOnElementAll(new CommunityMessageAuthor(device1, message)); + await sleepFor(500); // brief sleep to let the UI settle await device1.clickOnElementAll(new UPMMessageButton(device1)); await device1.clickOnElementAll(new ConversationHeaderName(device1, bob.userName)); await device1.waitForTextElementToBePresent(new MessageRequestPendingDescription(device1)); From f7351aa54f3fe45054e3a174be1b2ce6166c48d7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 09:23:28 +1100 Subject: [PATCH 047/184] feat: add dependabot monthly update --- .github/dependabot.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..abb0a12aa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# Get a single PR per month with all updates. +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + target-branch: "dev" + groups: + # One group to rule them all + monthly-updates: + patterns: + - "*" + + # Keep Node and pnpm pinned + ignore: + - dependency-name: "node" + - dependency-name: "pnpm" \ No newline at end of file From a1da68fec57ea31a4c35ac4abcd3eb279479582a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 11:33:46 +1100 Subject: [PATCH 048/184] feat: add ban test --- .github/workflows/android-regression.yml | 1 + run/test/specs/community_ban.spec.ts | 88 ++++++++++++++++++++++++ run/test/specs/utils/capabilities_ios.ts | 2 +- run/types/DeviceWrapper.ts | 2 +- run/types/testing.ts | 1 + 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 run/test/specs/community_ban.spec.ts diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index da72e8d23..6d1a91054 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -80,6 +80,7 @@ jobs: _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) + SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} steps: - uses: actions/checkout@v6 diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts new file mode 100644 index 000000000..fdbaaa15a --- /dev/null +++ b/run/test/specs/community_ban.spec.ts @@ -0,0 +1,88 @@ +import test, { type TestInfo } from '@playwright/test'; + +import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { User } from '../../types/testing'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; +import { ConversationItem } from './locators/home'; +import { newUser } from './utils/create_account'; +import { joinCommunity } from './utils/join_community'; +import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; +import { restoreAccount } from './utils/restore_account'; + +androidIt({ + title: 'Ban user in community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: banUserCommunity, +}); + +async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + if (!process.env.SOGS_ADMIN_SEED) { + throw new Error( + 'SOGS_ADMIN_SEED required. In CI this is a GitHub secret.\nLocally, set a known admin seed as an env var to run this test.' + ); + } + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban me - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED, + }; + const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); + await test.step('Restore admin account, create new account to be banned', async () => { + await Promise.all([ + restoreAccount(alice1, alice, 'alice1'), + newUser(bob1, 'Bob', { saveUserData: false }), + ]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banUser').toString(), + }); + await alice1.checkModalStrings( + englishStrippedStr('banUser').toString(), + englishStrippedStr('communityBanDescription').toString() + ); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step('Verify Bob cannot send messages in community', async () => { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 3e7258101..67dd24e12 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -25,7 +25,7 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { 'appium:deviceName': 'iPhone 17', 'appium:automationName': 'XCUITest', 'appium:bundleId': iOSBundleId, - 'appium:newCommandTimeout': 300000, + 'appium:newCommandTimeout': 600000, 'appium:useNewWDA': false, 'appium:showXcodeLog': false, 'appium:autoDismissAlerts': false, diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f55f1ba68..c3d66de8c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1641,7 +1641,7 @@ export class DeviceWrapper { // Success when element is GONE return { success: !element }; }, - { maxWait: 15_000 } + { maxWait: 18_000 } ); this.info('Loading animation has finished'); diff --git a/run/types/testing.ts b/run/types/testing.ts index 020c43d26..43919d7aa 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -530,6 +530,7 @@ export type Id = | 'network.loki.messenger:id/callSubtitle' | 'network.loki.messenger:id/callTitle' | 'network.loki.messenger:id/characterLimitText' + | 'network.loki.messenger:id/context_menu_item_title' | 'network.loki.messenger:id/crop_image_menu_crop' | 'network.loki.messenger:id/emptyStateContainer' | 'network.loki.messenger:id/endCallButton' From 16aebb9cc0bac3e9bc9ea3d56b6db175a2b10543 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 11:48:21 +1100 Subject: [PATCH 049/184] feat: add ban and delete test --- run/test/specs/community_ban.spec.ts | 92 +++++++++++++++++++++++++++- run/types/allure.ts | 1 + 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index fdbaaa15a..2e0d6098f 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -22,21 +22,44 @@ androidIt({ risk: 'medium', countOfDevicesNeeded: 2, testCb: banUserCommunity, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: + 'Verifies that a community admin can ban a user. Banned user cannot send messages anymore.', }); -async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { +androidIt({ + title: 'Ban and delete in community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: banAndDelete, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: + 'Verifies that a community admin can ban a user and delete their messages. Banned user cannot send messages anymore.', +}); + +function assertAdminIsKnown() { if (!process.env.SOGS_ADMIN_SEED) { throw new Error( 'SOGS_ADMIN_SEED required. In CI this is a GitHub secret.\nLocally, set a known admin seed as an env var to run this test.' ); } +} + +async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); const msgSig = `${new Date().getTime()} - ${platform}`; const msg1 = `Ban me - ${msgSig}`; const msg2 = `Am I banned? - ${msgSig}`; const alice: User = { userName: 'Alice', accountID: '', // Mandatory property of User type but not needed for this test - recoveryPhrase: process.env.SOGS_ADMIN_SEED, + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, }; const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); await test.step('Restore admin account, create new account to be banned', async () => { @@ -86,3 +109,68 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await closeApp(alice1, bob1); }); } + +async function banAndDelete(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban and delete - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, + }; + const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); + await test.step('Restore admin account, create new account to be banned', async () => { + await Promise.all([ + restoreAccount(alice1, alice, 'alice1'), + newUser(bob1, 'Bob', { saveUserData: false }), + ]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob and deletes all from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banDeleteAll').toString(), + }); + await alice1.checkModalStrings( + englishStrippedStr('banDeleteAll').toString(), + englishStrippedStr('communityBanDeleteDescription').toString() + ); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step(`Verify Bob's first message has been deleted`, async () => { + await alice1.verifyElementNotPresent({ + ...new MessageBody(alice1, msg1).build(), + maxWait: 5_000, + }); + }); + await test.step('Verify Bob cannot send messages in community', async () => { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/types/allure.ts b/run/types/allure.ts index 086411a16..9f4881329 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -35,6 +35,7 @@ export type AllureSuiteConfig = | { parent: 'User Actions'; suite: + | 'Ban/Unban' | 'Block/Unblock' | 'Change Profile Picture' | 'Change Username' From e8aa5663ab4b8458d7bec16905bd9db1da38a83c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 11:33:46 +1100 Subject: [PATCH 050/184] feat: add ban test --- .github/workflows/android-regression.yml | 1 + run/test/specs/community_ban.spec.ts | 88 ++++++++++++++++++++++++ run/test/specs/utils/capabilities_ios.ts | 2 +- run/types/DeviceWrapper.ts | 2 +- run/types/testing.ts | 1 + 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 run/test/specs/community_ban.spec.ts diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 94c0758bc..0867cb485 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -71,6 +71,7 @@ jobs: _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) + SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} steps: - uses: actions/checkout@v6 diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts new file mode 100644 index 000000000..fdbaaa15a --- /dev/null +++ b/run/test/specs/community_ban.spec.ts @@ -0,0 +1,88 @@ +import test, { type TestInfo } from '@playwright/test'; + +import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { User } from '../../types/testing'; +import { + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; +import { ConversationItem } from './locators/home'; +import { newUser } from './utils/create_account'; +import { joinCommunity } from './utils/join_community'; +import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; +import { restoreAccount } from './utils/restore_account'; + +androidIt({ + title: 'Ban user in community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: banUserCommunity, +}); + +async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + if (!process.env.SOGS_ADMIN_SEED) { + throw new Error( + 'SOGS_ADMIN_SEED required. In CI this is a GitHub secret.\nLocally, set a known admin seed as an env var to run this test.' + ); + } + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban me - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED, + }; + const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); + await test.step('Restore admin account, create new account to be banned', async () => { + await Promise.all([ + restoreAccount(alice1, alice, 'alice1'), + newUser(bob1, 'Bob', { saveUserData: false }), + ]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banUser').toString(), + }); + await alice1.checkModalStrings( + englishStrippedStr('banUser').toString(), + englishStrippedStr('communityBanDescription').toString() + ); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step('Verify Bob cannot send messages in community', async () => { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/test/specs/utils/capabilities_ios.ts b/run/test/specs/utils/capabilities_ios.ts index 3e7258101..67dd24e12 100644 --- a/run/test/specs/utils/capabilities_ios.ts +++ b/run/test/specs/utils/capabilities_ios.ts @@ -25,7 +25,7 @@ const sharediOSCapabilities: AppiumXCUITestCapabilities = { 'appium:deviceName': 'iPhone 17', 'appium:automationName': 'XCUITest', 'appium:bundleId': iOSBundleId, - 'appium:newCommandTimeout': 300000, + 'appium:newCommandTimeout': 600000, 'appium:useNewWDA': false, 'appium:showXcodeLog': false, 'appium:autoDismissAlerts': false, diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 18f159ab6..1c1b780fc 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1630,7 +1630,7 @@ export class DeviceWrapper { // Success when element is GONE return { success: !element }; }, - { maxWait: 15_000 } + { maxWait: 18_000 } ); this.info('Loading animation has finished'); diff --git a/run/types/testing.ts b/run/types/testing.ts index c1dfd538f..cff36906e 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -530,6 +530,7 @@ export type Id = | 'network.loki.messenger:id/callSubtitle' | 'network.loki.messenger:id/callTitle' | 'network.loki.messenger:id/characterLimitText' + | 'network.loki.messenger:id/context_menu_item_title' | 'network.loki.messenger:id/crop_image_menu_crop' | 'network.loki.messenger:id/emptyStateContainer' | 'network.loki.messenger:id/endCallButton' From 681bdbb78e3bc1f413398de3c02315395383bcb2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 11:48:21 +1100 Subject: [PATCH 051/184] feat: add ban and delete test --- run/test/specs/community_ban.spec.ts | 92 +++++++++++++++++++++++++++- run/types/allure.ts | 1 + 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index fdbaaa15a..2e0d6098f 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -22,21 +22,44 @@ androidIt({ risk: 'medium', countOfDevicesNeeded: 2, testCb: banUserCommunity, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: + 'Verifies that a community admin can ban a user. Banned user cannot send messages anymore.', }); -async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { +androidIt({ + title: 'Ban and delete in community', + risk: 'medium', + countOfDevicesNeeded: 2, + testCb: banAndDelete, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: + 'Verifies that a community admin can ban a user and delete their messages. Banned user cannot send messages anymore.', +}); + +function assertAdminIsKnown() { if (!process.env.SOGS_ADMIN_SEED) { throw new Error( 'SOGS_ADMIN_SEED required. In CI this is a GitHub secret.\nLocally, set a known admin seed as an env var to run this test.' ); } +} + +async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); const msgSig = `${new Date().getTime()} - ${platform}`; const msg1 = `Ban me - ${msgSig}`; const msg2 = `Am I banned? - ${msgSig}`; const alice: User = { userName: 'Alice', accountID: '', // Mandatory property of User type but not needed for this test - recoveryPhrase: process.env.SOGS_ADMIN_SEED, + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, }; const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); await test.step('Restore admin account, create new account to be banned', async () => { @@ -86,3 +109,68 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await closeApp(alice1, bob1); }); } + +async function banAndDelete(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban and delete - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, + }; + const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); + await test.step('Restore admin account, create new account to be banned', async () => { + await Promise.all([ + restoreAccount(alice1, alice, 'alice1'), + newUser(bob1, 'Bob', { saveUserData: false }), + ]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob and deletes all from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll({ + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banDeleteAll').toString(), + }); + await alice1.checkModalStrings( + englishStrippedStr('banDeleteAll').toString(), + englishStrippedStr('communityBanDeleteDescription').toString() + ); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step(`Verify Bob's first message has been deleted`, async () => { + await alice1.verifyElementNotPresent({ + ...new MessageBody(alice1, msg1).build(), + maxWait: 5_000, + }); + }); + await test.step('Verify Bob cannot send messages in community', async () => { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/types/allure.ts b/run/types/allure.ts index 62d0d00ec..020931d41 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -35,6 +35,7 @@ export type AllureSuiteConfig = | { parent: 'User Actions'; suite: + | 'Ban/Unban' | 'Block/Unblock' | 'Change Profile Picture' | 'Change Username' From 1c9b66e4d15ccea86a0fbc545eb45ddfd4ac1bd0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Feb 2026 16:15:41 +1100 Subject: [PATCH 052/184] feat: add device identity to restore account --- run/test/specs/linked_device_restore_group.spec.ts | 2 +- run/test/specs/utils/restore_account.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/run/test/specs/linked_device_restore_group.spec.ts b/run/test/specs/linked_device_restore_group.spec.ts index 289e53d83..54a14ba3c 100644 --- a/run/test/specs/linked_device_restore_group.spec.ts +++ b/run/test/specs/linked_device_restore_group.spec.ts @@ -34,7 +34,7 @@ async function restoreGroup(platform: SupportedPlatformsType, testInfo: TestInfo const aliceMessage = `${USERNAME.ALICE} to ${testGroupName}`; const bobMessage = `${USERNAME.BOB} to ${testGroupName}`; const charlieMessage = `${USERNAME.CHARLIE} to ${testGroupName}`; - await restoreAccount(device4, alice); + await restoreAccount(device4, alice, 'alice2'); // Check that group has loaded on linked device await device4.clickOnElementAll(new ConversationItem(device4, testGroupName)); // Check the group name has loaded diff --git a/run/test/specs/utils/restore_account.ts b/run/test/specs/utils/restore_account.ts index cb496910d..2ae005a8c 100644 --- a/run/test/specs/utils/restore_account.ts +++ b/run/test/specs/utils/restore_account.ts @@ -15,9 +15,11 @@ import { handleNotificationPermissions } from './permissions'; export const restoreAccount = async ( device: DeviceWrapper, user: User, + deviceIdentity: string, options?: BaseSetupOptions ) => { const { allowNotificationPermissions = false } = options || {}; + device.setDeviceIdentity(deviceIdentity); await device.clickOnElementAll(new AccountRestoreButton(device)); await device.inputText(user.recoveryPhrase, new SeedPhraseInput(device)); // Wait for continue button to become active From 9521922508822e9d57ddf5ce1b44e55c17e3d156 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Feb 2026 16:15:55 +1100 Subject: [PATCH 053/184] feat: add dependabot updates --- .github/dependabot.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index abb0a12aa..8096968fa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,18 +1,18 @@ # Get a single PR per month with all updates. version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "monthly" - target-branch: "dev" + interval: 'monthly' + target-branch: 'dev' groups: # One group to rule them all monthly-updates: patterns: - - "*" - - # Keep Node and pnpm pinned + - '*' + + # Keep Node and pnpm pinned ignore: - - dependency-name: "node" - - dependency-name: "pnpm" \ No newline at end of file + - dependency-name: 'node' + - dependency-name: 'pnpm' From ae8fc1de78cbc1d164351ca260c4429c613c7af3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Feb 2026 16:16:11 +1100 Subject: [PATCH 054/184] fix: ban and unban in one test --- run/test/specs/community_ban.spec.ts | 52 +++++++++++---------- run/test/specs/locators/conversation.ts | 62 +++++++++++++++++++++++-- run/types/testing.ts | 3 ++ 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 2e0d6098f..170ad805d 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -3,9 +3,12 @@ import test, { type TestInfo } from '@playwright/test'; import { testCommunityLink, testCommunityName } from '../../constants/community'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; -import { androidIt } from '../../types/sessionIt'; +import { bothPlatformsIt } from '../../types/sessionIt'; import { User } from '../../types/testing'; import { + LongPressBanAndDelete, + LongPressBanUser, + LongPressUnBan, MessageBody, MessageInput, OutgoingMessageStatusSent, @@ -17,8 +20,8 @@ import { joinCommunity } from './utils/join_community'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; import { restoreAccount } from './utils/restore_account'; -androidIt({ - title: 'Ban user in community', +bothPlatformsIt({ + title: 'Ban and unban user in community', risk: 'medium', countOfDevicesNeeded: 2, testCb: banUserCommunity, @@ -26,11 +29,12 @@ androidIt({ parent: 'User Actions', suite: 'Ban/Unban', }, - allureDescription: - 'Verifies that a community admin can ban a user. Banned user cannot send messages anymore.', + allureDescription: `Verifies that a community admin can ban a user. + Banned user cannot send messages anymore. + Admin then can unban a user and they can send messages again. `, }); -androidIt({ +bothPlatformsIt({ title: 'Ban and delete in community', risk: 'medium', countOfDevicesNeeded: 2, @@ -45,17 +49,18 @@ androidIt({ function assertAdminIsKnown() { if (!process.env.SOGS_ADMIN_SEED) { - throw new Error( - 'SOGS_ADMIN_SEED required. In CI this is a GitHub secret.\nLocally, set a known admin seed as an env var to run this test.' - ); + console.error('SOGS_ADMIN_SEED required. In CI this is a GitHub secret.'); + console.error('Locally, set a known admin seed as an env var to run this test.'); + test.skip(); } } async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { assertAdminIsKnown(); const msgSig = `${new Date().getTime()} - ${platform}`; - const msg1 = `Ban me - ${msgSig}`; + const msg1 = `Ban and unban me - ${msgSig}`; const msg2 = `Am I banned? - ${msgSig}`; + const msg3 = `Freedom! - ${msgSig}`; const alice: User = { userName: 'Alice', accountID: '', // Mandatory property of User type but not needed for this test @@ -85,15 +90,11 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test }); await test.step('Admin bans Bob from community', async () => { await alice1.longPressMessage(new MessageBody(alice1, msg1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'network.loki.messenger:id/context_menu_item_title', - text: englishStrippedStr('banUser').toString(), - }); - await alice1.checkModalStrings( - englishStrippedStr('banUser').toString(), - englishStrippedStr('communityBanDescription').toString() - ); + await alice1.clickOnElementAll(new LongPressBanUser(alice1)); + // await alice1.checkModalStrings( + // englishStrippedStr('banUser').toString(), + // englishStrippedStr('communityBanDescription').toString() + // ); await alice1.clickOnByAccessibilityID('Continue'); }); await test.step('Verify Bob cannot send messages in community', async () => { @@ -105,6 +106,13 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test }); await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); }); + await test.step('Admin unbans Bob, Bob can send a third message', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll(new LongPressUnBan(alice1)); + await alice1.clickOnByAccessibilityID('Continue'); + await bob1.sendMessage(msg3); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, msg3)); + }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); }); @@ -144,11 +152,7 @@ async function banAndDelete(platform: SupportedPlatformsType, testInfo: TestInfo }); await test.step('Admin bans Bob and deletes all from community', async () => { await alice1.longPressMessage(new MessageBody(alice1, msg1)); - await alice1.clickOnElementAll({ - strategy: 'id', - selector: 'network.loki.messenger:id/context_menu_item_title', - text: englishStrippedStr('banDeleteAll').toString(), - }); + await alice1.clickOnElementAll(new LongPressBanAndDelete(alice1)); await alice1.checkModalStrings( englishStrippedStr('banDeleteAll').toString(), englishStrippedStr('communityBanDeleteDescription').toString() diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 8018d14ea..8f7e3b150 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -149,6 +149,7 @@ export class ConversationSettings extends LocatorsInterface { } } } + export class DeleteContactConfirmButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -165,7 +166,6 @@ export class DeleteContactConfirmButton extends LocatorsInterface { } } } - export class DeleteContactMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -182,6 +182,7 @@ export class DeleteContactMenuItem extends LocatorsInterface { } } } + export class DeleteConversationMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -198,7 +199,6 @@ export class DeleteConversationMenuItem extends LocatorsInterface { } } } - export class DeleteConversationModalConfirm extends LocatorsInterface { public build() { switch (this.platform) { @@ -215,6 +215,7 @@ export class DeleteConversationModalConfirm extends LocatorsInterface { } } } + export class DeletedMessage extends LocatorsInterface { public build() { return { @@ -223,7 +224,6 @@ export class DeletedMessage extends LocatorsInterface { } as const; } } - export class DocumentMessage extends LocatorsInterface { public build() { switch (this.platform) { @@ -386,6 +386,7 @@ export class HideNoteToSelfConfirmButton extends LocatorsInterface { } } } + export class HideNoteToSelfMenuOption extends LocatorsInterface { public build() { switch (this.platform) { @@ -402,7 +403,6 @@ export class HideNoteToSelfMenuOption extends LocatorsInterface { } } } - export class ImagesFolderButton extends LocatorsInterface { public build() { return { @@ -412,6 +412,60 @@ export class ImagesFolderButton extends LocatorsInterface { } } +export class LongPressBanAndDelete extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banDeleteAll').toString(), + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Ban and Delete', + } as const; + } + } +} + +export class LongPressBanUser extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banUser').toString(), + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Ban User', + } as const; + } + } +} + +export class LongPressUnBan extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/context_menu_item_title', + text: englishStrippedStr('banUnbanUser').toString(), + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Unban User', + } as const; + } + } +} + export class MediaMessage extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/types/testing.ts b/run/types/testing.ts index cff36906e..2ae828537 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -196,6 +196,8 @@ export type AccessibilityId = | 'back' | 'Back' | 'BackButton' + | 'Ban and Delete' + | 'Ban User' | 'Blinded ID' | 'Block' | 'Block contacts - Navigation' @@ -407,6 +409,7 @@ export type AccessibilityId = | 'Terms of Service' | 'test_file, pdf' | 'Time selector' + | 'Unban User' | 'Unblock' | 'Untrusted attachment message' | 'Upload' From 2a6669ba180ef0ad48694803ec7d150043964e81 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 13:29:19 +1100 Subject: [PATCH 055/184] chore: bump strings --- run/localizer/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/localizer/lib b/run/localizer/lib index c0714a891..b50f68687 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit c0714a8916a38672584323e6084e8cedc36d7243 +Subproject commit b50f686879732299de63f40008066c9e04b40b9b From 4cf1b195449cc28bc5157a1dee598e948e2912cb Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 14:23:41 +1100 Subject: [PATCH 056/184] chore: export assertadminisknown method --- run/test/specs/community_ban.spec.ts | 10 +--------- run/test/specs/community_emoji_react.spec.ts | 2 +- run/test/specs/community_requests_off.spec.ts | 2 +- run/test/specs/community_requests_on.spec.ts | 2 +- run/test/specs/community_tests_image.spec.ts | 2 +- run/test/specs/community_tests_join.spec.ts | 2 +- run/test/specs/disappearing_community_invite.spec.ts | 2 +- run/test/specs/message_community_invitation.spec.ts | 2 +- .../specs/utils/{join_community.ts => community.ts} | 10 ++++++++++ 9 files changed, 18 insertions(+), 16 deletions(-) rename run/test/specs/utils/{join_community.ts => community.ts} (75%) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 170ad805d..5e86409c2 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -15,8 +15,8 @@ import { SendButton, } from './locators/conversation'; import { ConversationItem } from './locators/home'; +import { assertAdminIsKnown, joinCommunity } from './utils/community'; import { newUser } from './utils/create_account'; -import { joinCommunity } from './utils/join_community'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; import { restoreAccount } from './utils/restore_account'; @@ -47,14 +47,6 @@ bothPlatformsIt({ 'Verifies that a community admin can ban a user and delete their messages. Banned user cannot send messages anymore.', }); -function assertAdminIsKnown() { - if (!process.env.SOGS_ADMIN_SEED) { - console.error('SOGS_ADMIN_SEED required. In CI this is a GitHub secret.'); - console.error('Locally, set a known admin seed as an env var to run this test.'); - test.skip(); - } -} - async function banUserCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { assertAdminIsKnown(); const msgSig = `${new Date().getTime()} - ${platform}`; diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index b89207921..45681790a 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -5,7 +5,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { EmojiReactsPill, FirstEmojiReact, MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/community_requests_off.spec.ts b/run/test/specs/community_requests_off.spec.ts index 5575b0ea8..4919cdf1d 100644 --- a/run/test/specs/community_requests_off.spec.ts +++ b/run/test/specs/community_requests_off.spec.ts @@ -7,7 +7,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { CommunityMessageAuthor, UPMMessageButton } from './locators/conversation'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index cb81c9a11..4de4dfc35 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -17,7 +17,7 @@ import { MessageRequestsBanner } from './locators/home'; import { CommunityMessageRequestSwitch, PrivacyMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index d6e83c3a4..9587c2297 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -6,7 +6,7 @@ import { bothPlatformsIt } from '../../types/sessionIt'; import { MessageBody } from './locators/conversation'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index c693693f9..5c95f886a 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -6,7 +6,7 @@ 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 { joinCommunity } from './utils/community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 8bf16c480..d1e2aaaa1 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -13,7 +13,7 @@ import { GroupMember } from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; import { setDisappearingMessage } from './utils/set_disappearing_messages'; diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index 0b60ff8f9..47a65a95c 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -13,7 +13,7 @@ import { GroupMember } from './locators/groups'; import { ConversationItem } from './locators/home'; import { open_Alice1_Bob1_friends } from './state_builder'; import { sleepFor } from './utils'; -import { joinCommunity } from './utils/join_community'; +import { joinCommunity } from './utils/community'; import { closeApp, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/utils/join_community.ts b/run/test/specs/utils/community.ts similarity index 75% rename from run/test/specs/utils/join_community.ts rename to run/test/specs/utils/community.ts index 342d1d4a3..23c09a80c 100644 --- a/run/test/specs/utils/join_community.ts +++ b/run/test/specs/utils/community.ts @@ -1,3 +1,5 @@ +import test from '@playwright/test'; + import { DeviceWrapper } from '../../../types/DeviceWrapper'; import { CommunityInput, JoinCommunityButton } from '../locators'; import { ConversationHeaderName, EmptyConversation } from '../locators/conversation'; @@ -17,3 +19,11 @@ export const joinCommunity = async ( await device.verifyElementNotPresent(new EmptyConversation(device)); // checking that messages loaded already await device.scrollToBottom(); }; + +export function assertAdminIsKnown() { + if (!process.env.SOGS_ADMIN_SEED) { + console.error('SOGS_ADMIN_SEED required. In CI this is a GitHub secret.'); + console.error('Locally, set a known admin seed as an env var to run this test.'); + test.skip(); + } +} \ No newline at end of file From 0370e5d7580c4e450a3943c1cbf2f24263f7aa3d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 15:34:25 +1100 Subject: [PATCH 057/184] feat: ban linked device tests --- eslint.config.mjs | 3 +- run/test/specs/community_requests_off.spec.ts | 2 +- run/test/specs/community_requests_on.spec.ts | 2 +- .../specs/linked_device_community_ban.spec.ts | 210 ++++++++++++++++++ run/test/specs/locators/conversation.ts | 2 +- run/test/specs/utils/community.ts | 2 +- run/types/allure.ts | 1 + run/types/testing.ts | 3 +- 8 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 run/test/specs/linked_device_community_ban.spec.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index fccbb7454..c1cc0de28 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,10 @@ import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; import perfectionist from 'eslint-plugin-perfectionist'; import globals from 'globals'; import tseslint from 'typescript-eslint'; -export default tseslint.config( +export default defineConfig( { files: ['**/*.{ts,tsx,cts,mts,js,cjs,mjs}'], }, diff --git a/run/test/specs/community_requests_off.spec.ts b/run/test/specs/community_requests_off.spec.ts index 4919cdf1d..fc25e504a 100644 --- a/run/test/specs/community_requests_off.spec.ts +++ b/run/test/specs/community_requests_off.spec.ts @@ -6,8 +6,8 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { CommunityMessageAuthor, UPMMessageButton } from './locators/conversation'; import { sleepFor } from './utils'; -import { newUser } from './utils/create_account'; import { joinCommunity } from './utils/community'; +import { newUser } from './utils/create_account'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index 4de4dfc35..b8faec75d 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -16,8 +16,8 @@ import { import { MessageRequestsBanner } from './locators/home'; import { CommunityMessageRequestSwitch, PrivacyMenuItem, UserSettings } from './locators/settings'; import { sleepFor } from './utils'; -import { newUser } from './utils/create_account'; import { joinCommunity } from './utils/community'; +import { newUser } from './utils/create_account'; import { closeApp, openAppTwoDevices, SupportedPlatformsType } from './utils/open_app'; bothPlatformsIt({ diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts new file mode 100644 index 000000000..faa2cbb73 --- /dev/null +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -0,0 +1,210 @@ +import test, { type TestInfo } from '@playwright/test'; + +import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { englishStrippedStr } from '../../localizer/englishStrippedStr'; +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { User } from '../../types/testing'; +import { + EmptyConversation, + LongPressBanAndDelete, + LongPressBanUser, + LongPressUnBan, + MessageBody, + MessageInput, + OutgoingMessageStatusSent, + SendButton, +} from './locators/conversation'; +import { ConversationItem } from './locators/home'; +import { assertAdminIsKnown, joinCommunity } from './utils/community'; +import { newUser } from './utils/create_account'; +import { closeApp, openAppThreeDevices, SupportedPlatformsType } from './utils/open_app'; +import { restoreAccount } from './utils/restore_account'; + +bothPlatformsIt({ + title: 'Ban and unban user in community - linked device', + risk: 'medium', + countOfDevicesNeeded: 3, + testCb: banUnbanLinked, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: `Verifies that a community admin can ban a user. + The banned user cannot send a message. + The unbanned account is restored on a second device. + Admin then unbans the user, and they can send messages on both devices.`, +}); + +bothPlatformsIt({ + title: 'Ban and delete in community - linked device', + risk: 'medium', + countOfDevicesNeeded: 3, + testCb: banAndDeleteLinked, + allureSuites: { + parent: 'User Actions', + suite: 'Ban/Unban', + }, + allureDescription: `Verifies that a community admin can ban a user and delete their messages. + Then, restore the banned account on a second device. + The banned user cannot send messages anymore on either of their linked devices.`, +}); + +// Bob 1 + Bob 2 get banned by Alice the admin +async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban, link, unban - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const msg3 = `You'll never catch me alive! - ${msgSig}`; + const msg3Linked = `${msg3} - linked device`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, + }; + const { + device1: alice1, + device2: bob1, + device3: bob2, + } = await openAppThreeDevices(platform, testInfo); + const [, bob] = + await test.step('Restore admin account, create new account to be banned', async () => { + return await Promise.all([restoreAccount(alice1, alice, 'alice1'), newUser(bob1, 'Bob')]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll(new LongPressBanUser(alice1)); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step('Verify Bob cannot send messages in community on either device', async () => { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { + await restoreAccount(bob2, bob, 'bob2'); + await bob2.clickOnElementAll(new ConversationItem(alice1, 'testing-all-the-things')); // Since we're banned we don't get the "real" name + await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); + await bob2.waitForTextElementToBePresent({ + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText`, + text: englishStrippedStr('permissionsWriteCommunity').toString(), + }); + }); + await test.step('Admin unbans Bob, Bob can send a third message from both devices', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll(new LongPressUnBan(alice1)); + await alice1.clickOnByAccessibilityID('Continue'); + await Promise.all([bob1.sendMessage(msg3), bob2.sendMessage(msg3Linked)]); + await Promise.all([ + alice1.waitForTextElementToBePresent(new MessageBody(alice1, msg3)), + alice1.waitForTextElementToBePresent(new MessageBody(alice1, msg3Linked)), + ]); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(bob1, alice1); + }); +} + +// Bob 1 + Bob 2 get banned by Alice the admin +async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: TestInfo) { + assertAdminIsKnown(); + const msgSig = `${new Date().getTime()} - ${platform}`; + const msg1 = `Ban and delete linked - ${msgSig}`; + const msg2 = `Am I banned? - ${msgSig}`; + const alice: User = { + userName: 'Alice', + accountID: '', // Mandatory property of User type but not needed for this test + recoveryPhrase: process.env.SOGS_ADMIN_SEED!, + }; + const { + device1: alice1, + device2: bob1, + device3: bob2, + } = await openAppThreeDevices(platform, testInfo); + const [, bob] = + await test.step('Restore admin account, create new account to be banned', async () => { + return await Promise.all([restoreAccount(alice1, alice, 'alice1'), newUser(bob1, 'Bob')]); + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + const adminJoined = await alice1.doesElementExist( + new ConversationItem(alice1, testCommunityName) + ); + if (!adminJoined) { + await joinCommunity(alice1, testCommunityLink, testCommunityName); + } else { + await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.scrollToBottom(); + } + await joinCommunity(bob1, testCommunityLink, testCommunityName); + }); + await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { + await bob1.sendMessage(msg1); + }); + await test.step('Admin bans Bob and deletes all from community', async () => { + await alice1.longPressMessage(new MessageBody(alice1, msg1)); + await alice1.clickOnElementAll(new LongPressBanAndDelete(alice1)); + await alice1.clickOnByAccessibilityID('Continue'); + }); + await test.step(`Verify Bob's first message has been deleted`, async () => { + await alice1.verifyElementNotPresent({ + ...new MessageBody(alice1, msg1).build(), + maxWait: 5_000, + }); + }); + await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { + await restoreAccount(bob2, bob, 'bob2'); + await bob2.clickOnElementAll(new ConversationItem(alice1, 'testing-all-the-things')); // Since we're banned we don't get the "real" name + await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); + }); + await test.step('Verify Bob cannot send messages in community on either device', async () => { + if (platform === 'android') { + await Promise.all( + [bob1, bob2].map(async device => { + await device.inputText(msg2, new MessageInput(device)); + await device.clickOnElementAll(new SendButton(device)); + await device.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(device).build(), + maxWait: 10_000, + }); + }) + ); + } else { + await bob1.inputText(msg2, new MessageInput(bob1)); + await bob1.clickOnElementAll(new SendButton(bob1)); + await bob1.verifyElementNotPresent({ + ...new OutgoingMessageStatusSent(bob1).build(), + maxWait: 10_000, + }); + await bob2.waitForTextElementToBePresent({ + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText`, + text: englishStrippedStr('permissionsWriteCommunity').toString(), + }); + } + await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(bob1, alice1); + }); +} diff --git a/run/test/specs/locators/conversation.ts b/run/test/specs/locators/conversation.ts index 8f7e3b150..391051a5a 100644 --- a/run/test/specs/locators/conversation.ts +++ b/run/test/specs/locators/conversation.ts @@ -424,7 +424,7 @@ export class LongPressBanAndDelete extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Ban and Delete', + selector: 'Ban and Delete All', } as const; } } diff --git a/run/test/specs/utils/community.ts b/run/test/specs/utils/community.ts index 23c09a80c..8935fc97a 100644 --- a/run/test/specs/utils/community.ts +++ b/run/test/specs/utils/community.ts @@ -26,4 +26,4 @@ export function assertAdminIsKnown() { console.error('Locally, set a known admin seed as an env var to run this test.'); test.skip(); } -} \ No newline at end of file +} diff --git a/run/types/allure.ts b/run/types/allure.ts index 020931d41..f94b4eaac 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -68,6 +68,7 @@ export const TestSteps = { NEW_USER: 'Create new account', QA_SEEDER: 'Restore pre-seeded accounts', CLOSE_APP: 'Close app(s)', + RESTORE_ACCOUNT: (name: UserNameType) => `Restore ${name} on another device`, }, // Plus Button options NEW_CONVERSATION: { diff --git a/run/types/testing.ts b/run/types/testing.ts index 2ae828537..b73bb7d30 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -150,6 +150,7 @@ export type XPath = | `//XCUIElementTypeStaticText[contains(@name, '00:')]` | `//XCUIElementTypeStaticText[contains(@name, "Version")]` | `//XCUIElementTypeStaticText[starts-with(@name,'${string}')]` + | `//XCUIElementTypeStaticText` | `//XCUIElementTypeSwitch[@name="Read Receipts, Send read receipts in one-to-one chats."]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.LinearLayout/android.widget.LinearLayout[2]/android.widget.Button[1]` | `/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ListView/android.widget.LinearLayout` @@ -196,7 +197,7 @@ export type AccessibilityId = | 'back' | 'Back' | 'BackButton' - | 'Ban and Delete' + | 'Ban and Delete All' | 'Ban User' | 'Blinded ID' | 'Block' From 5a56b3879cfe4d00637f0d40cc5e3e235d74d47a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 15:59:22 +1100 Subject: [PATCH 058/184] chore: tidy up --- run/test/specs/community_ban.spec.ts | 8 ++++---- run/test/specs/linked_device_community_ban.spec.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 5e86409c2..42948aca2 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -83,10 +83,10 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await test.step('Admin bans Bob from community', async () => { await alice1.longPressMessage(new MessageBody(alice1, msg1)); await alice1.clickOnElementAll(new LongPressBanUser(alice1)); - // await alice1.checkModalStrings( - // englishStrippedStr('banUser').toString(), - // englishStrippedStr('communityBanDescription').toString() - // ); + await alice1.checkModalStrings( + englishStrippedStr('banUser').toString(), + englishStrippedStr('communityBanDescription').toString() + ); await alice1.clickOnByAccessibilityID('Continue'); }); await test.step('Verify Bob cannot send messages in community', async () => { diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index faa2cbb73..e66ac4ee9 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -32,7 +32,7 @@ bothPlatformsIt({ }, allureDescription: `Verifies that a community admin can ban a user. The banned user cannot send a message. - The unbanned account is restored on a second device. + The banned account is restored on a second device. Admin then unbans the user, and they can send messages on both devices.`, }); From 1abf5125fcd974a3be4db92594c5c788734a9e0b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 16:55:50 +1100 Subject: [PATCH 059/184] fix: check readonly only on ios --- run/test/specs/linked_device_community_ban.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index e66ac4ee9..d91fd0ed8 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -105,7 +105,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn await restoreAccount(bob2, bob, 'bob2'); await bob2.clickOnElementAll(new ConversationItem(alice1, 'testing-all-the-things')); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); - await bob2.waitForTextElementToBePresent({ + await bob2.onIOS().waitForTextElementToBePresent({ strategy: 'xpath', selector: `//XCUIElementTypeStaticText`, text: englishStrippedStr('permissionsWriteCommunity').toString(), From a5da63189e744fb4e1d6336ed4e6960142041f47 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 9 Feb 2026 17:00:56 +1100 Subject: [PATCH 060/184] fix: start headless and use sw rendering --- scripts/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 647c403e1..13c30f042 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -92,7 +92,7 @@ function start_with_snapshots() { # 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 & + DISPLAY=:0 emulator @emulator$i -no-window -gpu swiftshader_indirect -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & sleep 5 done From e8cb9e337cf65eabaa7711d54a760efebcff5742 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Feb 2026 08:45:23 +1100 Subject: [PATCH 061/184] Revert "fix: start headless and use sw rendering" This reverts commit a5da63189e744fb4e1d6336ed4e6960142041f47. --- scripts/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 13c30f042..647c403e1 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -92,7 +92,7 @@ function start_with_snapshots() { # Set window position sed -i "s/^window.x.*/window.x=$(( 100 + (i-1) * 400))/" "$EMU_CONFIG_FILE" - DISPLAY=:0 emulator @emulator$i -no-window -gpu swiftshader_indirect -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & + DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load & sleep 5 done From 4fd26a75538f1d7338bcc5cd3deb269c688f138a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Feb 2026 10:02:15 +1100 Subject: [PATCH 062/184] chore: add emulator health check --- .github/workflows/android-regression.yml | 6 ++ package.json | 1 + scripts/emulator_health.ts | 127 +++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 scripts/emulator_health.ts diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 6d1a91054..6355d1f18 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -209,6 +209,9 @@ jobs: PLATFORM: ${{ env.PLATFORM }} UPLOAD_IDENTIFIER: 'devices-1-test-run' + - name: Recover emulators if needed + run: pnpm recover-emulators + - name: Run the 2-devices tests ​​with 2 workers continue-on-error: true id: devices-2-test-run @@ -228,6 +231,9 @@ jobs: PLATFORM: ${{ env.PLATFORM }} UPLOAD_IDENTIFIER: 'devices-2-test-run' + - name: Recover emulators if needed + run: pnpm recover-emulators + - name: Run the other tests with 1 worker continue-on-error: true id: other-devices-test-run diff --git a/package.json b/package.json index ba75d6df8..ec7af3c9a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "cleanup-simulators": "npx ts-node scripts/cleanup_ios_simulators.ts", "create-simulators": "pnpm cleanup-simulators && npx ts-node scripts/create_ios_simulators.ts", + "recover-emulators": "npx ts-node scripts/emulator_health.ts", "lint": "pnpm prettier . --write --cache && pnpm eslint . --cache ", "lint-check": "pnpm prettier . --check && pnpm eslint .", "tsc": "tsc", diff --git a/scripts/emulator_health.ts b/scripts/emulator_health.ts new file mode 100644 index 000000000..6450f94e7 --- /dev/null +++ b/scripts/emulator_health.ts @@ -0,0 +1,127 @@ +import { sleepFor } from '../run/test/specs/utils'; +import { runScriptAndLog } from '../run/test/specs/utils/utilities'; + +const EMULATOR_CONFIG = { + 1: 5554, + 2: 5556, + 3: 5558, + 4: 5560, +} as const; + +async function getRunningEmulators(): Promise { + const output = await runScriptAndLog('adb devices'); + return output + .split('\n') + .map(line => { + // Match only lines with emulator-PORT followed by 'device' state + const match = line.match(/emulator-(\d+)\s+device$/); + return match ? parseInt(match[1]) : null; + }) + .filter((port): port is number => port !== null); +} + +function portToEmulatorNum(port: number): number | undefined { + const entry = Object.entries(EMULATOR_CONFIG).find(([_, p]) => p === port); + return entry ? parseInt(entry[0]) : undefined; +} + +async function getMissingEmulators(): Promise { + const running = await getRunningEmulators(); + const allNums = Object.keys(EMULATOR_CONFIG).map(Number); + const runningNums = running.map(portToEmulatorNum).filter((n): n is number => n !== undefined); + return allNums.filter(n => !runningNums.includes(n)); +} + +async function waitForEmulatorBoot(emulatorNum: number, timeoutMs: number = 30_0000): Promise { + const port = EMULATOR_CONFIG[emulatorNum as keyof typeof EMULATOR_CONFIG]; + const startTime = Date.now(); + const maxAttempts = Math.floor(timeoutMs / 5_000); + + console.log(`Waiting for emulator ${emulatorNum} to boot...`); + + for (let i = 0; i < maxAttempts; i++) { + try { + const result = await runScriptAndLog( + `adb -s emulator-${port} shell getprop sys.boot_completed 2>/dev/null`, + false + ); + + if (result.trim() === '1') { + const elapsed = Math.round((Date.now() - startTime) / 1000); + console.log(`Emulator ${emulatorNum} booted (${elapsed}s)`); + return true; + } + } catch { + // Emulator not ready yet + } + + await sleepFor(5_000); + } + + console.log(`Emulator ${emulatorNum} failed to boot within ${timeoutMs / 1000}s`); + return false; +} + +async function restartMissingEmulators(): Promise { + const missing = await getMissingEmulators(); + + if (missing.length === 0) { + console.log('All emulators running'); + return; + } + + console.log(`Missing emulators: ${missing.join(', ')}`); + console.log(`Restarting emulators: ${missing.join(', ')}`); + + for (const num of missing) { + const port = EMULATOR_CONFIG[num as keyof typeof EMULATOR_CONFIG]; + + // Kill if zombie process + try { + await runScriptAndLog(`adb -s emulator-${port} emu kill`, false); + await sleepFor(2_000); + } catch { + // Already dead, that's fine + } + + // Restart from snapshot (same as ci.sh start_with_snapshots) + const configFile = `$HOME/.android/avd/emulator${num}.avd/emulator-user.ini`; + const windowX = 100 + (num - 1) * 400; + + await runScriptAndLog( + `sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, + false + ); + + await runScriptAndLog( + `DISPLAY=:0 emulator @emulator${num} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load &`, + false + ); + + await sleepFor(5_000); + } + + console.log(`\nWaiting for ${missing.length} emulator(s) to boot...`); + + const bootResults = await Promise.all(missing.map(num => waitForEmulatorBoot(num))); + + if (bootResults.every(result => result)) { + console.log(`\nEmulators restarted and booted successfully`); } else { + console.log(`\nSome emulators failed to boot`); + throw new Error('Emulator recovery failed'); + } +} + +async function main(): Promise { + try { + await restartMissingEmulators(); + process.exit(0); + } catch (error) { + console.error('Recovery failed:', error); + process.exit(1); + } +} + +if (require.main === module) { + void main(); +} From cfe467702aca2cd2d7e9293a85756a4443c804f1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Feb 2026 10:36:21 +1100 Subject: [PATCH 063/184] fix: ban modal strings --- run/test/specs/community_ban.spec.ts | 16 ++++++++++++---- .../specs/linked_device_community_ban.spec.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 42948aca2..4627d5563 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -83,10 +83,12 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await test.step('Admin bans Bob from community', async () => { await alice1.longPressMessage(new MessageBody(alice1, msg1)); await alice1.clickOnElementAll(new LongPressBanUser(alice1)); - await alice1.checkModalStrings( - englishStrippedStr('banUser').toString(), - englishStrippedStr('communityBanDescription').toString() - ); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Ban User'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('banUser').toString(), + englishStrippedStr('communityBanUserDescription').toString() + ); + }); await alice1.clickOnByAccessibilityID('Continue'); }); await test.step('Verify Bob cannot send messages in community', async () => { @@ -101,6 +103,12 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await test.step('Admin unbans Bob, Bob can send a third message', async () => { await alice1.longPressMessage(new MessageBody(alice1, msg1)); await alice1.clickOnElementAll(new LongPressUnBan(alice1)); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Unban User'), async () => { + await alice1.checkModalStrings( + englishStrippedStr('banUser').toString(), + englishStrippedStr('communityUnbanUserDescription').toString() + ); + }); await alice1.clickOnByAccessibilityID('Continue'); await bob1.sendMessage(msg3); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, msg3)); diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index d91fd0ed8..00ad4c5c5 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -92,7 +92,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn await alice1.clickOnElementAll(new LongPressBanUser(alice1)); await alice1.clickOnByAccessibilityID('Continue'); }); - await test.step('Verify Bob cannot send messages in community on either device', async () => { + await test.step('Verify Bob cannot send messages to community', async () => { await bob1.inputText(msg2, new MessageInput(bob1)); await bob1.clickOnElementAll(new SendButton(bob1)); await bob1.verifyElementNotPresent({ From 30dd6c92436ba9ea9998c61e10157ab87c0c063d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Feb 2026 10:40:20 +1100 Subject: [PATCH 064/184] chore: update landing page screenshot --- run/constants/community.ts | 1 + run/screenshots/android/landingpage_new_account.png | 4 ++-- run/test/specs/linked_device_community_ban.spec.ts | 10 +++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/run/constants/community.ts b/run/constants/community.ts index 04a67d580..5a575110c 100644 --- a/run/constants/community.ts +++ b/run/constants/community.ts @@ -1,2 +1,3 @@ export const testCommunityLink = `https://chat.lokinet.dev/testing-all-the-things?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17`; export const testCommunityName = `Testing All The Things!`; +export const unresolvedTestcommunityName = 'testing-all-the-things'; diff --git a/run/screenshots/android/landingpage_new_account.png b/run/screenshots/android/landingpage_new_account.png index 5a5de8b36..276e5373b 100644 --- a/run/screenshots/android/landingpage_new_account.png +++ b/run/screenshots/android/landingpage_new_account.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec8609e7d0013725d3be235b552a9399e94e2143f3edbeda4c7172c1a29f0ff -size 103496 +oid sha256:6cceb03631b9144e157f1c068f5917f58042c567adea70f3a9d74a9a478d9f02 +size 136014 diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index 00ad4c5c5..57cdd6bcc 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -1,6 +1,10 @@ import test, { type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { + testCommunityLink, + testCommunityName, + unresolvedTestcommunityName, +} from '../../constants/community'; import { englishStrippedStr } from '../../localizer/englishStrippedStr'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; @@ -103,7 +107,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, 'testing-all-the-things')); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); await bob2.onIOS().waitForTextElementToBePresent({ strategy: 'xpath', @@ -174,7 +178,7 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, 'testing-all-the-things')); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); }); await test.step('Verify Bob cannot send messages in community on either device', async () => { From 6d36e829e6a231a81ab88e5d0b86425bb74f8e39 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Feb 2026 13:41:32 +1100 Subject: [PATCH 065/184] fix: wait longer for promotion sent not present --- run/test/specs/group_tests_promote.spec.ts | 7 ++++--- scripts/emulator_health.ts | 13 +++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index 0442056cb..18c1709d1 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -329,9 +329,10 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T await Promise.all([ alice1.waitForTextElementToBePresent(new Contact(alice1, bob.userName)), alice1.waitForTextElementToBePresent(new Contact(alice1, charlie.userName)), - alice1.verifyElementNotPresent( - new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()) - ), + alice1.verifyElementNotPresent({ + ...new MemberStatus(alice1).build(englishStrippedStr('adminPromotionSent').toString()), + maxWait: 10_000, + }), alice1.verifyElementNotPresent( new MemberStatus(alice1).build(englishStrippedStr('adminPromotionFailed').toString()) ), diff --git a/scripts/emulator_health.ts b/scripts/emulator_health.ts index 6450f94e7..7d0f6c91b 100644 --- a/scripts/emulator_health.ts +++ b/scripts/emulator_health.ts @@ -32,7 +32,10 @@ async function getMissingEmulators(): Promise { return allNums.filter(n => !runningNums.includes(n)); } -async function waitForEmulatorBoot(emulatorNum: number, timeoutMs: number = 30_0000): Promise { +async function waitForEmulatorBoot( + emulatorNum: number, + timeoutMs: number = 30_0000 +): Promise { const port = EMULATOR_CONFIG[emulatorNum as keyof typeof EMULATOR_CONFIG]; const startTime = Date.now(); const maxAttempts = Math.floor(timeoutMs / 5_000); @@ -88,10 +91,7 @@ async function restartMissingEmulators(): Promise { const configFile = `$HOME/.android/avd/emulator${num}.avd/emulator-user.ini`; const windowX = 100 + (num - 1) * 400; - await runScriptAndLog( - `sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, - false - ); + await runScriptAndLog(`sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, false); await runScriptAndLog( `DISPLAY=:0 emulator @emulator${num} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load &`, @@ -106,7 +106,8 @@ async function restartMissingEmulators(): Promise { const bootResults = await Promise.all(missing.map(num => waitForEmulatorBoot(num))); if (bootResults.every(result => result)) { - console.log(`\nEmulators restarted and booted successfully`); } else { + console.log(`\nEmulators restarted and booted successfully`); + } else { console.log(`\nSome emulators failed to boot`); throw new Error('Emulator recovery failed'); } From 85decf7fd0cf70d67786bbd5b4a5f5233583046c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Feb 2026 14:48:28 +1100 Subject: [PATCH 066/184] chore: update string --- run/localizer/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/localizer/lib b/run/localizer/lib index b50f68687..f2620de9c 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit b50f686879732299de63f40008066c9e04b40b9b +Subproject commit f2620de9c8dc757ae7a131c55f60cdfd0074f47f From e4d89c4ac019c80d23e53bbc33b9203755cec2e3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Feb 2026 14:51:53 +1100 Subject: [PATCH 067/184] fix: use name in modals --- run/test/specs/community_ban.spec.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 4627d5563..09188917e 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -59,12 +59,13 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test recoveryPhrase: process.env.SOGS_ADMIN_SEED!, }; const { device1: alice1, device2: bob1 } = await openAppTwoDevices(platform, testInfo); - await test.step('Restore admin account, create new account to be banned', async () => { - await Promise.all([ - restoreAccount(alice1, alice, 'alice1'), - newUser(bob1, 'Bob', { saveUserData: false }), - ]); - }); + const [, bob] = + await test.step('Restore admin account, create new account to be banned', async () => { + return Promise.all([ + restoreAccount(alice1, alice, 'alice1'), + newUser(bob1, 'Bob', { saveUserData: false }), + ]); + }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( new ConversationItem(alice1, testCommunityName) @@ -86,7 +87,9 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Ban User'), async () => { await alice1.checkModalStrings( englishStrippedStr('banUser').toString(), - englishStrippedStr('communityBanUserDescription').toString() + englishStrippedStr('communityBanUserDescription') + .withArgs({ name: bob.userName }) + .toString() ); }); await alice1.clickOnByAccessibilityID('Continue'); @@ -106,7 +109,9 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Unban User'), async () => { await alice1.checkModalStrings( englishStrippedStr('banUser').toString(), - englishStrippedStr('communityUnbanUserDescription').toString() + englishStrippedStr('communityUnbanUserDescription') + .withArgs({ name: bob.userName }) + .toString() ); }); await alice1.clickOnByAccessibilityID('Continue'); From c521a06429535ea691c163955b87ff7f02c4c419 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Feb 2026 17:03:04 +1100 Subject: [PATCH 068/184] chore: fix modal heading --- run/test/specs/community_ban.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 09188917e..b698f0e8d 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -108,7 +108,7 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test await alice1.clickOnElementAll(new LongPressUnBan(alice1)); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Unban User'), async () => { await alice1.checkModalStrings( - englishStrippedStr('banUser').toString(), + englishStrippedStr('banUnbanUser').toString(), englishStrippedStr('communityUnbanUserDescription') .withArgs({ name: bob.userName }) .toString() From e457dce0dfa8bcecf40d854f8c152cf0519c42b6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Feb 2026 17:33:42 +1100 Subject: [PATCH 069/184] fix: prevent emulator recovery from hanging --- scripts/emulator_health.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/emulator_health.ts b/scripts/emulator_health.ts index 7d0f6c91b..931c37f0f 100644 --- a/scripts/emulator_health.ts +++ b/scripts/emulator_health.ts @@ -94,7 +94,7 @@ async function restartMissingEmulators(): Promise { await runScriptAndLog(`sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, false); await runScriptAndLog( - `DISPLAY=:0 emulator @emulator${num} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load &`, + `DISPLAY=:0 nohup emulator @emulator${num} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load > /dev/null 2>&1 &`, false ); @@ -113,13 +113,22 @@ async function restartMissingEmulators(): Promise { } } +const SCRIPT_TIMEOUT_MS = 5 * 60_000; // 5 minutes + async function main(): Promise { + const timeout = setTimeout(() => { + console.error(`Script timed out after ${SCRIPT_TIMEOUT_MS / 1000}s`); + process.exit(1); + }, SCRIPT_TIMEOUT_MS); + try { await restartMissingEmulators(); process.exit(0); } catch (error) { console.error('Recovery failed:', error); process.exit(1); + } finally { + clearTimeout(timeout); } } From 247bbc7a8a453d18447348251be0b22ac97b445d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Feb 2026 17:56:01 +1100 Subject: [PATCH 070/184] fix: add perms to reg workflows --- .github/workflows/android-regression.yml | 3 +++ .github/workflows/ios-regression.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 6355d1f18..afbb72464 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -62,6 +62,9 @@ on: - 'verbose' # Ongoing and failed test logs default: 'minimal' +permissions: + contents: write + jobs: android-regression: runs-on: [self-hosted, linux, X64, qa-android] diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 5b32722fa..a17a462a4 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -63,6 +63,9 @@ on: - 'verbose' # All test logs - use with caution! default: 'minimal' +permissions: + contents: write + jobs: ios-regression: runs-on: [self-hosted, macOS] From 7e6bcf263351b28ff21ad00d1df88373d76b83ab Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 09:04:15 +1100 Subject: [PATCH 071/184] fix: just check convo disappears of linked device --- .../specs/linked_device_block_user.spec.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 0dae1bf67..40360fbc4 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -43,23 +43,11 @@ async function blockUserInConversationOptions( ); // 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 alice2.hasElementBeenDeleted(new ConversationItem(alice2, bob.userName)) await alice1.navigateBack(); - // Look for alert at top of screen (Bob is blocked. Unblock them?) - // Check device 1 for blocked status - const blockedStatus = await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); - if (blockedStatus) { - // Check linked device for blocked status (if shown on alice1) - await alice2.onAndroid().clickOnElementAll(new ConversationItem(alice2, bob.userName)); - await alice2.onAndroid().waitForTextElementToBePresent(new BlockedBanner(alice2)); - alice2.info(`${bob.userName}` + ' has been blocked'); - } else { - alice2.info('Blocked banner not found'); - } + await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); // Check settings for blocked user - await Promise.all([alice1.navigateBack(), alice2.onAndroid().navigateBack()]); + await alice1.navigateBack(); await Promise.all([ alice1.clickOnElementAll(new UserSettings(alice1)), alice2.clickOnElementAll(new UserSettings(alice2)), From 1141a06ab6ca124ad4722df99f9bc64cd2fd3ef6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:07:35 +0000 Subject: [PATCH 072/184] chore(deps): bump the monthly-updates group with 13 updates Bumps the monthly-updates group with 13 updates: | Package | From | To | | --- | --- | --- | | [@playwright/test](https://github.com/microsoft/playwright) | `1.58.1` | `1.58.2` | | [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver) | `10.19.1` | `10.21.2` | | [dotenv](https://github.com/motdotla/dotenv) | `17.2.3` | `17.2.4` | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.2` | `10.0.1` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.0` | `25.2.3` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.54.0` | `8.55.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.54.0` | `8.55.0` | | [@wdio/types](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-types) | `9.23.3` | `9.24.0` | | [eslint](https://github.com/eslint/eslint) | `9.39.2` | `10.0.0` | | [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) | `5.4.0` | `5.5.0` | | [glob](https://github.com/isaacs/node-glob) | `13.0.1` | `13.0.2` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.54.0` | `8.55.0` | | [undici](https://github.com/nodejs/undici) | `7.20.0` | `7.21.0` | Updates `@playwright/test` from 1.58.1 to 1.58.2 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.58.1...v1.58.2) Updates `appium-xcuitest-driver` from 10.19.1 to 10.21.2 - [Release notes](https://github.com/appium/appium-xcuitest-driver/releases) - [Changelog](https://github.com/appium/appium-xcuitest-driver/blob/master/CHANGELOG.md) - [Commits](https://github.com/appium/appium-xcuitest-driver/compare/v10.19.1...v10.21.2) Updates `dotenv` from 17.2.3 to 17.2.4 - [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/motdotla/dotenv/commits) Updates `@eslint/js` from 9.39.2 to 10.0.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/HEAD/packages/js) Updates `@types/node` from 25.2.0 to 25.2.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@typescript-eslint/eslint-plugin` from 8.54.0 to 8.55.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.55.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.54.0 to 8.55.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.55.0/packages/parser) Updates `@wdio/types` from 9.23.3 to 9.24.0 - [Release notes](https://github.com/webdriverio/webdriverio/releases) - [Changelog](https://github.com/webdriverio/webdriverio/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio/webdriverio/commits/v9.24.0/packages/wdio-types) Updates `eslint` from 9.39.2 to 10.0.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.0) Updates `eslint-plugin-perfectionist` from 5.4.0 to 5.5.0 - [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases) - [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md) - [Commits](https://github.com/azat-io/eslint-plugin-perfectionist/compare/v5.4.0...v5.5.0) Updates `glob` from 13.0.1 to 13.0.2 - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v13.0.1...v13.0.2) Updates `typescript-eslint` from 8.54.0 to 8.55.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.55.0/packages/typescript-eslint) Updates `undici` from 7.20.0 to 7.21.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.20.0...v7.21.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.58.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: appium-xcuitest-driver dependency-version: 10.21.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: dotenv dependency-version: 17.2.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: "@eslint/js" dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: monthly-updates - dependency-name: "@types/node" dependency-version: 25.2.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.55.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.55.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: "@wdio/types" dependency-version: 9.24.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: eslint dependency-version: 10.0.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: monthly-updates - dependency-name: eslint-plugin-perfectionist dependency-version: 5.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: glob dependency-version: 13.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: typescript-eslint dependency-version: 8.55.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: undici dependency-version: 7.21.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates ... Signed-off-by: dependabot[bot] --- package.json | 26 +- pnpm-lock.yaml | 636 +++++++++++++++++++++++-------------------------- 2 files changed, 316 insertions(+), 346 deletions(-) diff --git a/package.json b/package.json index ec7af3c9a..1b0ff2902 100644 --- a/package.json +++ b/package.json @@ -27,24 +27,24 @@ "@appium/images-plugin": "^4.1.0", "@appium/opencv": "4.1.0", "@appium/types": "^1.2.0", - "@eslint/js": "^9.39.2", + "@eslint/js": "^10.0.1", "@types/fs-extra": "^11.0.4", "@types/gh-pages": "^6.1.0", "@types/lodash": "^4.17.23", - "@types/node": "^25.2.0", + "@types/node": "^25.2.3", "@types/sinon": "^21.0.0", - "@typescript-eslint/eslint-plugin": "^8.54.0", - "@typescript-eslint/parser": "^8.54.0", - "@wdio/types": "^9.23.3", + "@typescript-eslint/eslint-plugin": "^8.55.0", + "@typescript-eslint/parser": "^8.55.0", + "@wdio/types": "^9.24.0", "allure-commandline": "^2.36.0", "allure-js-commons": "^3.4.5", "allure-playwright": "^3.4.5", - "eslint": "^9.39.2", - "eslint-plugin-perfectionist": "^5.4.0", + "eslint": "^10.0.0", + "eslint-plugin-perfectionist": "^5.5.0", "fs-extra": "^11.3.3", "fuse.js": "^7.1.0", "gh-pages": "^6.3.0", - "glob": "^13.0.1", + "glob": "^13.0.2", "globals": "^17.3.0", "lodash": "^4.17.23", "looks-same": "^10.0.1", @@ -56,8 +56,8 @@ "ssim.js": "^3.5.0", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", - "undici": "^7.20.0", + "typescript-eslint": "^8.55.0", + "undici": "^7.21.0", "uuid": "^13.0.0" }, "license": "MIT", @@ -67,13 +67,13 @@ }, "dependencies": { "@appium/support": "^7.0.5", - "@playwright/test": "^1.58.1", + "@playwright/test": "^1.58.2", "@session-foundation/playwright-reporter": "^0.0.8", "@session-foundation/qa-seeder": "^0.1.26", "appium": "^3.2.0", "appium-uiautomator2-driver": "^6.8.0", - "appium-xcuitest-driver": "^10.19.1", - "dotenv": "^17.2.3" + "appium-xcuitest-driver": "^10.21.2", + "dotenv": "^17.2.4" }, "packageManager": "pnpm@10.28.1", "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eddae006b..165f2e352 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: ^7.0.5 version: 7.0.5 '@playwright/test': - specifier: ^1.58.1 - version: 1.58.1 + specifier: ^1.58.2 + version: 1.58.2 '@session-foundation/playwright-reporter': specifier: ^0.0.8 version: 0.0.8 @@ -38,11 +38,11 @@ importers: specifier: ^6.8.0 version: 6.8.0(patch_hash=8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121)(appium@3.2.0) appium-xcuitest-driver: - specifier: ^10.19.1 - version: 10.19.1(appium@3.2.0) + specifier: ^10.21.2 + version: 10.21.2(appium@3.2.0) dotenv: - specifier: ^17.2.3 - version: 17.2.3 + specifier: ^17.2.4 + version: 17.2.4 devDependencies: '@appium/execute-driver-plugin': specifier: ^5.1.0 @@ -57,8 +57,8 @@ importers: specifier: ^1.2.0 version: 1.2.0 '@eslint/js': - specifier: ^9.39.2 - version: 9.39.2 + specifier: ^10.0.1 + version: 10.0.1(eslint@10.0.0) '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -69,35 +69,35 @@ importers: specifier: ^4.17.23 version: 4.17.23 '@types/node': - specifier: ^25.2.0 - version: 25.2.0 + specifier: ^25.2.3 + version: 25.2.3 '@types/sinon': specifier: ^21.0.0 version: 21.0.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.54.0 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + specifier: ^8.55.0 + version: 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.54.0 - version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) + specifier: ^8.55.0 + version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) '@wdio/types': - specifier: ^9.23.3 - version: 9.23.3 + specifier: ^9.24.0 + version: 9.24.0 allure-commandline: specifier: ^2.36.0 version: 2.36.0 allure-js-commons: specifier: ^3.4.5 - version: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.1)) + version: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) allure-playwright: specifier: ^3.4.5 - version: 3.4.5(@playwright/test@1.58.1) + version: 3.4.5(@playwright/test@1.58.2) eslint: - specifier: ^9.39.2 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.0 eslint-plugin-perfectionist: - specifier: ^5.4.0 - version: 5.4.0(eslint@9.39.2)(typescript@5.9.3) + specifier: ^5.5.0 + version: 5.5.0(eslint@10.0.0)(typescript@5.9.3) fs-extra: specifier: ^11.3.3 version: 11.3.3 @@ -108,8 +108,8 @@ importers: specifier: ^6.3.0 version: 6.3.0 glob: - specifier: ^13.0.1 - version: 13.0.1 + specifier: ^13.0.2 + version: 13.0.2 globals: specifier: ^17.3.0 version: 17.3.0 @@ -139,16 +139,16 @@ importers: version: 3.5.0 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.2.0)(typescript@5.9.3) + version: 10.9.2(@types/node@25.2.3)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.54.0 - version: 8.54.0(eslint@9.39.2)(typescript@5.9.3) + specifier: ^8.55.0 + version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) undici: - specifier: ^7.20.0 - version: 7.20.0 + specifier: ^7.21.0 + version: 7.21.0 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -240,33 +240,34 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.1': + resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.2': + resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.0': + resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.1': + resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.0': + resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -462,8 +463,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.1': - resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true @@ -537,6 +538,9 @@ packages: '@tsconfig/node20@20.1.8': resolution: {integrity: sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -555,11 +559,11 @@ packages: '@types/lodash@4.17.23': resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} - '@types/node@20.19.31': - resolution: {integrity: sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==} + '@types/node@20.19.33': + resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} - '@types/node@25.2.0': - resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/node@25.2.3': + resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -585,63 +589,63 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.54.0': - resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + '@typescript-eslint/eslint-plugin@8.55.0': + resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.54.0 + '@typescript-eslint/parser': ^8.55.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.54.0': - resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + '@typescript-eslint/parser@8.55.0': + resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.54.0': - resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.54.0': - resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.54.0': - resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.54.0': - resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + '@typescript-eslint/type-utils@8.55.0': + resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.54.0': - resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.54.0': - resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.54.0': - resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.54.0': - resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@wdio/config@9.23.2': @@ -663,8 +667,8 @@ packages: resolution: {integrity: sha512-ryfrERGsNp+aCcrTE1rFU6cbmDj8GHZ04R9k52KNt2u1a6bv3Eh5A/cUA0hXuMdEUfsc8ePLYdwQyOLFydZ0ig==} engines: {node: '>=18.20.0'} - '@wdio/types@9.23.3': - resolution: {integrity: sha512-Ufjh06DAD7cGTMORUkq5MTZLw1nAgBSr2y8OyiNNuAfPGCwHEU3EwEfhG/y0V7S7xT5pBxliqWi7AjRrCgGcIA==} + '@wdio/types@9.24.0': + resolution: {integrity: sha512-PYYunNl8Uq1r8YMJAK6ReRy/V/XIrCSyj5cpCtR5EqCL6heETOORFj7gt4uPnzidfgbtMBcCru0LgjjlMiH1UQ==} engines: {node: '>=18.20.0'} '@wdio/utils@9.23.2': @@ -778,8 +782,8 @@ packages: resolution: {integrity: sha512-j4zNwszDvBHqyZKqX99RLcif3CnuYDpVKA9E2DBM/4mOFYT+o3bPAMrPZRx2Tt3RImoTCUrTCUinYopsPqX2Eg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-ios-remotexpc@0.28.1: - resolution: {integrity: sha512-UzrIsD+EpWS4GttJqQcrySyMgQvltkne7wBM1SDXldkiFufKx+RYy+ldnjFi1rMOo66IrvAtL4KhxjBDzxgJ4Q==} + appium-ios-remotexpc@0.29.0: + resolution: {integrity: sha512-TrVaAdLiyEl5dviFGGaPH1Vi+bXWgXUFrBCQ7isRnS6ukyPAyGxpR81im/V5MKc4TYuU2w3gg+gdV4zQ/a98Jg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} appium-ios-simulator@8.0.11: @@ -790,8 +794,8 @@ packages: resolution: {integrity: sha512-UZYWTIQrdKU3nwL9YjlQG19LWIzTs0CeG1FdgZXUZRu699z7rTJJu/d6JOuHNf/akj3BXRmbItOvllrDqEIz6A==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-remote-debugger@15.3.2: - resolution: {integrity: sha512-7NQpoq0NNEWpxKMUXs3Yhd2dO6dH1mGsVLnIRJpBI2QyidDxgK1DX4ixwCv1Oh9BXeux+3N32gDE2cXxcOZfdQ==} + appium-remote-debugger@15.3.4: + resolution: {integrity: sha512-BZlNJ7qHL17JJame18JEKQDgfP7vFjViPZNCifIKRQU4KRaCQmEV4I5S+0TpBUFm0dG44cbWOp1Sfui7XyxzVw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} appium-uiautomator2-driver@6.8.0: @@ -812,8 +816,8 @@ packages: resolution: {integrity: sha512-nk86u0wo4ZPCxQsiF/1PR/HewpB5NxSkNxUttRLiWEbMTA8FPqDlbUfrD/xaywMYUYkYIk+W9ufz3Gg0CKMBAA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-xcuitest-driver@10.19.1: - resolution: {integrity: sha512-cfFMsNnhcGmwFLhJZswQ2hgXdAWLs/AU+/NY2xoItqMftFN6jj+TjmIKzeGVFfQwi7d7nctP1iDPkvLJU2cR7A==} + appium-xcuitest-driver@10.21.2: + resolution: {integrity: sha512-CodjJD+bQC4oyHmbyuIyN9+bGpzBy+S2MmX7P/GL0AV0FvWYwNde+4wxdX421O14s8q94fHIPNeX+4AsEdYnng==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} peerDependencies: appium: ^3.0.0-rc.2 @@ -875,6 +879,9 @@ packages: axios@1.13.4: resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + b4a@1.7.3: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: @@ -962,9 +969,6 @@ packages: resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} engines: {node: '>= 5.10.0'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -1000,10 +1004,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1106,9 +1106,6 @@ packages: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1280,8 +1277,8 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.2.4: + resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -1383,15 +1380,15 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-plugin-perfectionist@5.4.0: - resolution: {integrity: sha512-XxpUMpeVaSJF5rpF6NHmhj3xavHZrflKcRbDssAUWrHUU/+l3l7PPYnVJ6IOpR2KjQ1Blucaeb0cFL3LIBis0A==} + eslint-plugin-perfectionist@5.5.0: + resolution: {integrity: sha512-lZX2KUpwOQf7J27gAg/6vt8ugdPULOLmelM8oDJPMbaN7P2zNNeyS9yxGSmJcKX0SF9qR/962l9RWM2Z5jpPzg==} engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: eslint: '>=8.45.0' - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.0: + resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} @@ -1401,9 +1398,13 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.0: + resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.0: + resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -1411,9 +1412,9 @@ packages: jiti: optional: true - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.1.0: + resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -1662,14 +1663,10 @@ packages: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} - glob@13.0.1: - resolution: {integrity: sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==} + glob@13.0.2: + resolution: {integrity: sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==} engines: {node: 20 || >=22} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globals@17.3.0: resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} engines: {node: '>=18'} @@ -1760,10 +1757,6 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -1860,16 +1853,20 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - js2xmlparser2@0.2.0: resolution: {integrity: sha512-SzFGc1hQqzpDcalKmrM5gobSMGRSRg2lgaZrHGIfowrmd8+uaI+PWW62jcCGIqI+b4wdyYK0VKMhvVtJfkD0cg==} @@ -1957,9 +1954,6 @@ packages: lodash.isfinite@3.3.2: resolution: {integrity: sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.zip@4.2.0: resolution: {integrity: sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==} @@ -1996,6 +1990,10 @@ packages: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} engines: {node: 20 || >=22} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -2069,9 +2067,6 @@ packages: resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -2259,10 +2254,6 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-color@1.0.0: resolution: {integrity: sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==} @@ -2333,13 +2324,13 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - playwright-core@1.58.1: - resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.1: - resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -2455,10 +2446,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2528,6 +2515,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -2697,10 +2689,6 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strip-outer@1.0.1: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} @@ -2820,8 +2808,8 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript-eslint@8.54.0: - resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} + typescript-eslint@8.55.0: + resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2845,8 +2833,8 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} - undici@7.20.0: - resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} + undici@7.21.0: + resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} unicorn-magic@0.3.0: @@ -2952,6 +2940,11 @@ packages: engines: {node: ^20.17.0 || >=22.9.0} hasBin: true + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + winston-transport@4.9.0: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} @@ -3240,50 +3233,38 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)': dependencies: - eslint: 9.39.2 + eslint: 10.0.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.23.1': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.1 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 10.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.2': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 - '@eslint/core@0.17.0': + '@eslint/core@1.1.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} + '@eslint/js@10.0.1(eslint@10.0.0)': + optionalDependencies: + eslint: 10.0.0 - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@3.0.1': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.6.0': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -3434,9 +3415,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.1': + '@playwright/test@1.58.2': dependencies: - playwright: 1.58.1 + playwright: 1.58.2 '@promptbook/utils@0.69.5': dependencies: @@ -3464,7 +3445,7 @@ snapshots: extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 - semver: 7.7.3 + semver: 7.7.4 tar-fs: 3.1.1 yargs: 17.7.2 transitivePeerDependencies: @@ -3538,12 +3519,14 @@ snapshots: '@tsconfig/node20@20.1.8': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 25.2.0 + '@types/node': 25.2.3 '@types/gh-pages@6.1.0': {} @@ -3551,15 +3534,15 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.3 '@types/lodash@4.17.23': {} - '@types/node@20.19.31': + '@types/node@20.19.33': dependencies: undici-types: 6.21.0 - '@types/node@25.2.0': + '@types/node@25.2.3': dependencies: undici-types: 7.16.0 @@ -3579,22 +3562,22 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.3 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.3 optional: true - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.2 + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + eslint: 10.0.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -3602,79 +3585,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 - eslint: 9.39.2 + eslint: 10.0.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.54.0': + '@typescript-eslint/scope-manager@8.55.0': dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 - '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2 + eslint: 10.0.0 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.54.0': {} + '@typescript-eslint/types@8.55.0': {} - '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.2)(typescript@5.9.3)': + '@typescript-eslint/utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.2 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + eslint: 10.0.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.54.0': + '@typescript-eslint/visitor-keys@8.55.0': dependencies: - '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/types': 8.55.0 eslint-visitor-keys: 4.2.1 '@wdio/config@9.23.2': @@ -3703,15 +3686,15 @@ snapshots: '@wdio/repl@9.16.2': dependencies: - '@types/node': 20.19.31 + '@types/node': 20.19.33 '@wdio/types@9.23.2': dependencies: - '@types/node': 20.19.31 + '@types/node': 20.19.33 - '@wdio/types@9.23.3': + '@wdio/types@9.24.0': dependencies: - '@types/node': 20.19.31 + '@types/node': 20.19.33 '@wdio/utils@9.23.2': dependencies: @@ -3783,16 +3766,16 @@ snapshots: allure-commandline@2.36.0: {} - allure-js-commons@3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.1)): + allure-js-commons@3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)): dependencies: md5: 2.3.0 optionalDependencies: - allure-playwright: 3.4.5(@playwright/test@1.58.1) + allure-playwright: 3.4.5(@playwright/test@1.58.2) - allure-playwright@3.4.5(@playwright/test@1.58.1): + allure-playwright@3.4.5(@playwright/test@1.58.2): dependencies: - '@playwright/test': 1.58.1 - allure-js-commons: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.1)) + '@playwright/test': 1.58.2 + allure-js-commons: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) ansi-regex@5.0.1: {} @@ -3812,8 +3795,8 @@ snapshots: bluebird: 3.7.2 ini: 6.0.0 lodash: 4.17.23 - lru-cache: 11.2.5 - semver: 7.7.3 + lru-cache: 11.2.6 + semver: 7.7.4 teen_process: 4.0.9 transitivePeerDependencies: - bare-abort-controller @@ -3832,11 +3815,11 @@ snapshots: bluebird: 3.7.2 io.appium.settings: 7.0.18 lodash: 4.17.23 - lru-cache: 11.2.5 + lru-cache: 11.2.6 moment: 2.30.1 moment-timezone: 0.6.0 portscanner: 2.2.0 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 ws: 8.19.0 transitivePeerDependencies: @@ -3858,7 +3841,7 @@ snapshots: bluebird: 3.7.2 compare-versions: 6.1.1 lodash: 4.17.23 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 xpath: 0.0.34 transitivePeerDependencies: @@ -3883,25 +3866,25 @@ snapshots: dependencies: '@appium/support': 7.0.5 asyncbox: 6.1.0 - axios: 1.13.4 + axios: 1.13.5 bluebird: 3.7.2 bplist-creator: 0.1.1 bplist-parser: 0.3.2 lodash: 4.17.23 - semver: 7.7.3 + semver: 7.7.4 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - appium-ios-remotexpc@0.28.1: + appium-ios-remotexpc@0.29.0: dependencies: '@appium/strongbox': 1.0.1 '@appium/support': 7.0.5 - '@types/node': 25.2.0 + '@types/node': 25.2.3 '@xmldom/xmldom': 0.9.8 appium-ios-tuntap: 0.1.3 - axios: 1.13.4 + axios: 1.13.5 minimatch: 10.1.2 npm-run-all2: 8.0.4 transitivePeerDependencies: @@ -3920,7 +3903,7 @@ snapshots: bluebird: 3.7.2 lodash: 4.17.23 node-simctl: 8.1.5 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 transitivePeerDependencies: - bare-abort-controller @@ -3938,7 +3921,7 @@ snapshots: - react-native-b4a optional: true - appium-remote-debugger@15.3.2: + appium-remote-debugger@15.3.4: dependencies: '@appium/base-driver': 10.2.0 '@appium/support': 7.0.5 @@ -3946,7 +3929,7 @@ snapshots: async-lock: 1.4.1 asyncbox: 6.1.0 bluebird: 3.7.2 - glob: 13.0.1 + glob: 13.0.2 lodash: 4.17.23 teen_process: 4.0.9 transitivePeerDependencies: @@ -3988,7 +3971,7 @@ snapshots: appium-ios-simulator: 8.0.11 async-lock: 1.4.1 asyncbox: 6.1.0 - axios: 1.13.4 + axios: 1.13.5 bluebird: 3.7.2 lodash: 4.17.23 teen_process: 4.0.9 @@ -4005,14 +3988,14 @@ snapshots: bluebird: 3.7.2 lodash: 4.17.23 plist: 3.1.0 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - appium-xcuitest-driver@10.19.1(appium@3.2.0): + appium-xcuitest-driver@10.21.2(appium@3.2.0): dependencies: '@appium/strongbox': 1.0.1 '@colors/colors': 1.6.0 @@ -4020,7 +4003,7 @@ snapshots: appium-idb: 2.0.8 appium-ios-device: 3.1.9 appium-ios-simulator: 8.0.11 - appium-remote-debugger: 15.3.2 + appium-remote-debugger: 15.3.4 appium-webdriveragent: 11.1.4 appium-xcode: 6.1.8 async-lock: 1.4.1 @@ -4030,18 +4013,18 @@ snapshots: css-selector-parser: 3.3.0 js2xmlparser2: 0.2.0 lodash: 4.17.23 - lru-cache: 11.2.5 + lru-cache: 11.2.6 moment: 2.30.1 moment-timezone: 0.6.0 node-devicectl: 1.1.4 node-simctl: 8.1.5 portscanner: 2.2.0 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 winston: 3.19.0 ws: 8.19.0 optionalDependencies: - appium-ios-remotexpc: 0.28.1 + appium-ios-remotexpc: 0.29.0 transitivePeerDependencies: - bare-abort-controller - bufferutil @@ -4153,6 +4136,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + b4a@1.7.3: {} balanced-match@1.0.2: {} @@ -4238,11 +4229,6 @@ snapshots: dependencies: big-integer: 1.6.52 - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -4279,8 +4265,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4310,7 +4294,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.20.0 + undici: 7.21.0 whatwg-mimetype: 4.0.0 chromium-bidi@0.5.8(devtools-protocol@0.0.1232444): @@ -4389,8 +4373,6 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 - concat-map@0.0.1: {} - consola@3.4.2: {} console-control-strings@1.1.0: {} @@ -4524,7 +4506,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv@17.2.3: {} + dotenv@17.2.4: {} dunder-proto@1.0.1: dependencies: @@ -4550,7 +4532,7 @@ snapshots: fast-xml-parser: 5.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - which: 6.0.0 + which: 6.0.1 transitivePeerDependencies: - supports-color @@ -4618,17 +4600,19 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-perfectionist@5.4.0(eslint@9.39.2)(typescript@5.9.3): + eslint-plugin-perfectionist@5.5.0(eslint@10.0.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + eslint: 10.0.0 natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-scope@8.4.0: + eslint-scope@9.1.0: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -4636,28 +4620,27 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.2: + eslint-visitor-keys@5.0.0: {} + + eslint@10.0.0: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.1 + '@eslint/config-helpers': 0.5.2 + '@eslint/core': 1.1.0 + '@eslint/plugin-kit': 0.6.0 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.12.6 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.0 + eslint-visitor-keys: 5.0.0 + espree: 11.1.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -4668,18 +4651,17 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.1.2 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: - supports-color - espree@10.4.0: + espree@11.1.0: dependencies: acorn: 8.15.0 acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 + eslint-visitor-keys: 5.0.0 esprima@4.0.1: {} @@ -4967,14 +4949,12 @@ snapshots: minipass: 7.1.2 path-scurry: 2.0.1 - glob@13.0.1: + glob@13.0.2: dependencies: minimatch: 10.1.2 minipass: 7.1.2 path-scurry: 2.0.1 - globals@14.0.0: {} - globals@17.3.0: {} globby@11.1.0: @@ -5009,7 +4989,7 @@ snapshots: hosted-git-info@9.0.2: dependencies: - lru-cache: 11.2.5 + lru-cache: 11.2.6 hpack.js@2.1.6: dependencies: @@ -5071,11 +5051,6 @@ snapshots: immediate@3.0.6: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -5092,7 +5067,7 @@ snapshots: asyncbox: 6.1.0 bluebird: 3.7.2 lodash: 4.17.23 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 ip-address@10.1.0: {} @@ -5139,6 +5114,11 @@ snapshots: isexe@3.1.1: {} + isexe@3.1.5: + optional: true + + isexe@4.0.0: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -5147,10 +5127,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - js2xmlparser2@0.2.0: {} jsftp@2.1.3(supports-color@10.2.2): @@ -5243,8 +5219,6 @@ snapshots: lodash.isfinite@3.3.2: {} - lodash.merge@4.6.2: {} - lodash.zip@4.2.0: {} lodash@4.17.23: {} @@ -5284,6 +5258,8 @@ snapshots: lru-cache@11.2.5: {} + lru-cache@11.2.6: {} + lru-cache@7.18.3: {} make-dir@3.1.0: @@ -5346,10 +5322,6 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.1 - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - minimatch@5.1.6: dependencies: brace-expansion: 2.0.2 @@ -5420,10 +5392,10 @@ snapshots: bluebird: 3.7.2 lodash: 4.17.23 rimraf: 6.1.2 - semver: 7.7.3 + semver: 7.7.4 teen_process: 4.0.9 uuid: 13.0.0 - which: 6.0.0 + which: 6.0.1 normalize-package-data@8.0.0: dependencies: @@ -5554,10 +5526,6 @@ snapshots: pako@1.0.11: {} - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-color@1.0.0: dependencies: color-convert: 0.5.3 @@ -5596,7 +5564,7 @@ snapshots: path-scurry@2.0.1: dependencies: - lru-cache: 11.2.5 + lru-cache: 11.2.6 minipass: 7.1.2 path-to-regexp@8.3.0: {} @@ -5618,11 +5586,11 @@ snapshots: dependencies: find-up: 4.1.0 - playwright-core@1.58.1: {} + playwright-core@1.58.2: {} - playwright@1.58.1: + playwright@1.58.2: dependencies: - playwright-core: 1.58.1 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -5659,7 +5627,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -5780,8 +5748,6 @@ snapshots: require-from-string@2.0.2: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resq@1.11.0: @@ -5801,7 +5767,7 @@ snapshots: rimraf@6.1.2: dependencies: - glob: 13.0.1 + glob: 13.0.2 package-json-from-dist: 1.0.1 router@2.2.0: @@ -5843,6 +5809,8 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -6090,8 +6058,6 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} - strip-outer@1.0.1: dependencies: escape-string-regexp: 1.0.5 @@ -6175,14 +6141,14 @@ snapshots: dependencies: typescript: 5.9.3 - ts-node@10.9.2(@types/node@25.2.0)(typescript@5.9.3): + ts-node@10.9.2(@types/node@25.2.3)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.2.0 + '@types/node': 25.2.3 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -6217,13 +6183,13 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.54.0(eslint@9.39.2)(typescript@5.9.3): + typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + eslint: 10.0.0 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6242,7 +6208,7 @@ snapshots: undici@6.23.0: {} - undici@7.20.0: {} + undici@7.21.0: {} unicorn-magic@0.3.0: {} @@ -6297,7 +6263,7 @@ snapshots: webdriver@9.23.2: dependencies: - '@types/node': 20.19.31 + '@types/node': 20.19.33 '@types/ws': 8.18.1 '@wdio/config': 9.23.2 '@wdio/logger': 9.18.0 @@ -6318,7 +6284,7 @@ snapshots: webdriverio@9.23.2(puppeteer-core@21.11.0): dependencies: - '@types/node': 20.19.31 + '@types/node': 20.19.33 '@types/sinonjs__fake-timers': 8.1.5 '@wdio/config': 9.23.2 '@wdio/logger': 9.18.0 @@ -6374,13 +6340,17 @@ snapshots: which@5.0.0: dependencies: - isexe: 3.1.1 + isexe: 3.1.5 optional: true which@6.0.0: dependencies: isexe: 3.1.1 + which@6.0.1: + dependencies: + isexe: 4.0.0 + winston-transport@4.9.0: dependencies: logform: 2.7.0 From 47113a77e4e9da283dd8eb7ab0b570dc8ef93ee6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 09:47:34 +1100 Subject: [PATCH 073/184] refactor: parallelize linked device leave group --- .../specs/linked_device_block_user.spec.ts | 2 +- run/test/specs/linked_group_leave.spec.ts | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/run/test/specs/linked_device_block_user.spec.ts b/run/test/specs/linked_device_block_user.spec.ts index 40360fbc4..c43e9797a 100644 --- a/run/test/specs/linked_device_block_user.spec.ts +++ b/run/test/specs/linked_device_block_user.spec.ts @@ -43,7 +43,7 @@ async function blockUserInConversationOptions( ); // Confirm block option await alice1.clickOnElementAll(new BlockUserConfirmationModal(alice1)); - await alice2.hasElementBeenDeleted(new ConversationItem(alice2, bob.userName)) + await alice2.hasElementBeenDeleted(new ConversationItem(alice2, bob.userName)); await alice1.navigateBack(); await alice1.waitForTextElementToBePresent(new BlockedBanner(alice1)); // Check settings for blocked user diff --git a/run/test/specs/linked_group_leave.spec.ts b/run/test/specs/linked_group_leave.spec.ts index 4a0b16229..7138266e9 100644 --- a/run/test/specs/linked_group_leave.spec.ts +++ b/run/test/specs/linked_group_leave.spec.ts @@ -6,7 +6,6 @@ import { USERNAME } from '../../types/testing'; import { ConversationSettings } from './locators/conversation'; import { LeaveGroupConfirm, LeaveGroupMenuItem } from './locators/groups'; import { ConversationItem } from './locators/home'; -import { sleepFor } from './utils'; import { newUser } from './utils/create_account'; import { createGroup } from './utils/create_group'; import { linkedDevice } from './utils/link_device'; @@ -27,29 +26,32 @@ bothPlatformsIt({ async function leaveGroupLinkedDevice(platform: SupportedPlatformsType, testInfo: TestInfo) { const testGroupName = 'Leave group linked device'; const { device1, device2, device3, device4 } = await openAppFourDevices(platform, testInfo); - const charlie = await linkedDevice(device3, device4, USERNAME.CHARLIE); - // Create users A, B and C - const [alice, bob] = await Promise.all([ + const [alice, bob, charlie] = await Promise.all([ newUser(device1, USERNAME.ALICE), newUser(device2, USERNAME.BOB), + linkedDevice(device3, device4, USERNAME.CHARLIE), ]); // Create group with user A, user B and User C await createGroup(platform, device1, alice, device2, bob, device3, charlie, testGroupName); - await sleepFor(1000); + // If we know group is present on device4, we can check for just disappearance later (vs. hasElementBeenDeleted) + await device4.waitForTextElementToBePresent(new ConversationItem(device2, testGroupName)); + // Leave Group on device 3 await device3.clickOnElementAll(new ConversationSettings(device3)); - await sleepFor(1000); 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 group disappearing - await Promise.all([ - device3.verifyElementNotPresent(new ConversationItem(device3, testGroupName)), - device4.hasElementBeenDeleted(new ConversationItem(device4, testGroupName)), - ]); + // Check for group not being visible anymore + await Promise.all( + [device3, device4].map(device => + device.verifyElementNotPresent({ + ...new ConversationItem(device, testGroupName).build(), + maxWait: 10_000, + }) + ) + ); // Create control message for user leaving group const groupMemberLeft = englishStrippedStr('groupMemberLeft') .withArgs({ name: charlie.userName }) From 08fa1ddab92e6e2113d6278a796cb9a7774d3cb5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 10:42:13 +1100 Subject: [PATCH 074/184] chore: satisfy linter this eliminates the `no-useless-assignment` and `preserve-caught-error` issues --- run/test/specs/utils/handle_first_open.ts | 3 +-- run/test/specs/utils/open_app.ts | 2 +- run/types/DeviceWrapper.ts | 20 +++++++++----------- tsconfig.json | 2 +- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/run/test/specs/utils/handle_first_open.ts b/run/test/specs/utils/handle_first_open.ts index 5c6a79fbb..e6cff9512 100644 --- a/run/test/specs/utils/handle_first_open.ts +++ b/run/test/specs/utils/handle_first_open.ts @@ -37,8 +37,7 @@ export async function handlePhotosFirstTimeOpen(device: DeviceWrapper) { // On Android, the Photos app shows a sign-in prompt the first time it's opened that needs to be dismissed // I've seen two different kinds of sign in buttons on the same set of emulators if (device.isAndroid()) { - let signInButton = null; - signInButton = await device.doesElementExist({ + let signInButton = await device.doesElementExist({ strategy: 'id', selector: 'com.google.android.apps.photos:id/sign_in_button', maxWait: 1_000, diff --git a/run/test/specs/utils/open_app.ts b/run/test/specs/utils/open_app.ts index 263616be8..1324213ea 100644 --- a/run/test/specs/utils/open_app.ts +++ b/run/test/specs/utils/open_app.ts @@ -194,7 +194,7 @@ async function isEmulatorRunning(emulatorName: string) { async function waitForEmulatorToBeRunning(emulatorName: string) { let start = Date.now(); - let found = false; + let found: boolean; do { found = await isEmulatorRunning(emulatorName); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index c3d66de8c..304d5312e 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -423,7 +423,7 @@ export class DeviceWrapper { } // Validate the candidate element - let isValidCandidate = true; + let isValidCandidate: boolean; // Always check visibility first try { @@ -614,7 +614,9 @@ export class DeviceWrapper { skipHealing: true, }); } catch (fallbackError) { - throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`); + throw new Error(`Element ${primaryDescription} and ${fallbackDescription} not found.`, { + cause: fallbackError, + }); } } } @@ -666,10 +668,8 @@ export class DeviceWrapper { public async clickOnElementAll( args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) ) { - let el: AppiumNextElementType | null = null; const locator = args instanceof LocatorsInterface ? args.build() : args; - - el = await this.waitForTextElementToBePresent({ ...locator }); + const el = await this.waitForTextElementToBePresent({ ...locator }); await this.click(el.ELEMENT); return el; } @@ -880,10 +880,8 @@ export class DeviceWrapper { public async deleteText( args: LocatorsInterface | ({ text?: string; maxWait?: number } & StrategyExtractionObj) ) { - let el: AppiumNextElementType | null = null; const locator = args instanceof LocatorsInterface ? args.build() : args; - - el = await this.waitForTextElementToBePresent({ ...locator }); + const el = await this.waitForTextElementToBePresent({ ...locator }); await this.click(el.ELEMENT); await sleepFor(100); const maxRetries = 3; @@ -1242,6 +1240,7 @@ export class DeviceWrapper { } catch (e) { // Stale reference or other error checking visibility const errorMsg = e instanceof Error ? e.message : String(e); + // eslint-disable-next-line preserve-caught-error -- error message already included above throw new Error( `Element with ${description} has stale reference or error checking visibility: ${errorMsg}` ); @@ -1662,7 +1661,7 @@ export class DeviceWrapper { } = {} ): Promise { const start = Date.now(); - let elapsed = 0; + let elapsed: number; let attempt = 0; let lastError: string | undefined; @@ -1835,12 +1834,11 @@ export class DeviceWrapper { textToInput: string, args: LocatorsInterface | ({ maxWait?: number } & StrategyExtractionObj) ) { - let el: AppiumNextElementType | null = null; const locator = args instanceof LocatorsInterface ? args.build() : args; this.log('Locator being used:', locator); - el = await this.waitForTextElementToBePresent({ ...locator }); + const el = await this.waitForTextElementToBePresent({ ...locator }); if (!el) { throw new Error(`inputText: Did not find element with locator: ${JSON.stringify(locator)}`); } diff --git a/tsconfig.json b/tsconfig.json index ccdc2c1b8..a842d7e6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "strictNullChecks": true, "esModuleInterop": true, "types": ["@wdio/types", "node"], - "target": "es2021", + "target": "es2022", "outDir": "./dist", "rootDir": "./", "isolatedModules": false From 9ab38d9c11560a1f0b537c95ac327b35ddd83e10 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 13:20:32 +1100 Subject: [PATCH 075/184] feat: make user pro via dev backend --- english_wordlist.txt | 1626 ++++++++++++++++++++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 22 +- run/constants/index.ts | 2 + run/test/utils/mock_pro.ts | 394 +++++++++ 5 files changed, 2045 insertions(+), 1 deletion(-) create mode 100644 english_wordlist.txt create mode 100644 run/test/utils/mock_pro.ts diff --git a/english_wordlist.txt b/english_wordlist.txt new file mode 100644 index 000000000..9b92e9344 --- /dev/null +++ b/english_wordlist.txt @@ -0,0 +1,1626 @@ +abbey +abducts +ability +ablaze +abnormal +abort +abrasive +absorb +abyss +academy +aces +aching +acidic +acoustic +acquire +across +actress +acumen +adapt +addicted +adept +adhesive +adjust +adopt +adrenalin +adult +adventure +aerial +afar +affair +afield +afloat +afoot +afraid +after +against +agenda +aggravate +agile +aglow +agnostic +agony +agreed +ahead +aided +ailments +aimless +airport +aisle +ajar +akin +alarms +album +alchemy +alerts +algebra +alkaline +alley +almost +aloof +alpine +already +also +altitude +alumni +always +amaze +ambush +amended +amidst +ammo +amnesty +among +amply +amused +anchor +android +anecdote +angled +ankle +annoyed +answers +antics +anvil +anxiety +anybody +apart +apex +aphid +aplomb +apology +apply +apricot +aptitude +aquarium +arbitrary +archer +ardent +arena +argue +arises +army +around +arrow +arsenic +artistic +ascend +ashtray +aside +asked +asleep +aspire +assorted +asylum +athlete +atlas +atom +atrium +attire +auburn +auctions +audio +august +aunt +austere +autumn +avatar +avidly +avoid +awakened +awesome +awful +awkward +awning +awoken +axes +axis +axle +aztec +azure +baby +bacon +badge +baffles +bagpipe +bailed +bakery +balding +bamboo +banjo +baptism +basin +batch +bawled +bays +because +beer +befit +begun +behind +being +below +bemused +benches +berries +bested +betting +bevel +beware +beyond +bias +bicycle +bids +bifocals +biggest +bikini +bimonthly +binocular +biology +biplane +birth +biscuit +bite +biweekly +blender +blip +bluntly +boat +bobsled +bodies +bogeys +boil +boldly +bomb +border +boss +both +bounced +bovine +bowling +boxes +boyfriend +broken +brunt +bubble +buckets +budget +buffet +bugs +building +bulb +bumper +bunch +business +butter +buying +buzzer +bygones +byline +bypass +cabin +cactus +cadets +cafe +cage +cajun +cake +calamity +camp +candy +casket +catch +cause +cavernous +cease +cedar +ceiling +cell +cement +cent +certain +chlorine +chrome +cider +cigar +cinema +circle +cistern +citadel +civilian +claim +click +clue +coal +cobra +cocoa +code +coexist +coffee +cogs +cohesive +coils +colony +comb +cool +copy +corrode +costume +cottage +cousin +cowl +criminal +cube +cucumber +cuddled +cuffs +cuisine +cunning +cupcake +custom +cycling +cylinder +cynical +dabbing +dads +daft +dagger +daily +damp +dangerous +dapper +darted +dash +dating +dauntless +dawn +daytime +dazed +debut +decay +dedicated +deepest +deftly +degrees +dehydrate +deity +dejected +delayed +demonstrate +dented +deodorant +depth +desk +devoid +dewdrop +dexterity +dialect +dice +diet +different +digit +dilute +dime +dinner +diode +diplomat +directed +distance +ditch +divers +dizzy +doctor +dodge +does +dogs +doing +dolphin +domestic +donuts +doorway +dormant +dosage +dotted +double +dove +down +dozen +dreams +drinks +drowning +drunk +drying +dual +dubbed +duckling +dude +duets +duke +dullness +dummy +dunes +duplex +duration +dusted +duties +dwarf +dwelt +dwindling +dying +dynamite +dyslexic +each +eagle +earth +easy +eating +eavesdrop +eccentric +echo +eclipse +economics +ecstatic +eden +edgy +edited +educated +eels +efficient +eggs +egotistic +eight +either +eject +elapse +elbow +eldest +eleven +elite +elope +else +eluded +emails +ember +emerge +emit +emotion +empty +emulate +energy +enforce +enhanced +enigma +enjoy +enlist +enmity +enough +enraged +ensign +entrance +envy +epoxy +equip +erase +erected +erosion +error +eskimos +espionage +essential +estate +etched +eternal +ethics +etiquette +evaluate +evenings +evicted +evolved +examine +excess +exhale +exit +exotic +exquisite +extra +exult +fabrics +factual +fading +fainted +faked +fall +family +fancy +farming +fatal +faulty +fawns +faxed +fazed +feast +february +federal +feel +feline +females +fences +ferry +festival +fetches +fever +fewest +fiat +fibula +fictional +fidget +fierce +fifteen +fight +films +firm +fishing +fitting +five +fixate +fizzle +fleet +flippant +flying +foamy +focus +foes +foggy +foiled +folding +fonts +foolish +fossil +fountain +fowls +foxes +foyer +framed +friendly +frown +fruit +frying +fudge +fuel +fugitive +fully +fuming +fungal +furnished +fuselage +future +fuzzy +gables +gadget +gags +gained +galaxy +gambit +gang +gasp +gather +gauze +gave +gawk +gaze +gearbox +gecko +geek +gels +gemstone +general +geometry +germs +gesture +getting +geyser +ghetto +ghost +giant +giddy +gifts +gigantic +gills +gimmick +ginger +girth +giving +glass +gleeful +glide +gnaw +gnome +goat +goblet +godfather +goes +goggles +going +goldfish +gone +goodbye +gopher +gorilla +gossip +gotten +gourmet +governing +gown +greater +grunt +guarded +guest +guide +gulp +gumball +guru +gusts +gutter +guys +gymnast +gypsy +gyrate +habitat +hacksaw +haggled +hairy +hamburger +happens +hashing +hatchet +haunted +having +hawk +haystack +hazard +hectare +hedgehog +heels +hefty +height +hemlock +hence +heron +hesitate +hexagon +hickory +hiding +highway +hijack +hiker +hills +himself +hinder +hippo +hire +history +hitched +hive +hoax +hobby +hockey +hoisting +hold +honked +hookup +hope +hornet +hospital +hotel +hounded +hover +howls +hubcaps +huddle +huge +hull +humid +hunter +hurried +husband +huts +hybrid +hydrogen +hyper +iceberg +icing +icon +identity +idiom +idled +idols +igloo +ignore +iguana +illness +imagine +imbalance +imitate +impel +inactive +inbound +incur +industrial +inexact +inflamed +ingested +initiate +injury +inkling +inline +inmate +innocent +inorganic +input +inquest +inroads +insult +intended +inundate +invoke +inwardly +ionic +irate +iris +irony +irritate +island +isolated +issued +italics +itches +items +itinerary +itself +ivory +jabbed +jackets +jaded +jagged +jailed +jamming +january +jargon +jaunt +javelin +jaws +jazz +jeans +jeers +jellyfish +jeopardy +jerseys +jester +jetting +jewels +jigsaw +jingle +jittery +jive +jobs +jockey +jogger +joining +joking +jolted +jostle +journal +joyous +jubilee +judge +juggled +juicy +jukebox +july +jump +junk +jury +justice +juvenile +kangaroo +karate +keep +kennel +kept +kernels +kettle +keyboard +kickoff +kidneys +king +kiosk +kisses +kitchens +kiwi +knapsack +knee +knife +knowledge +knuckle +koala +laboratory +ladder +lagoon +lair +lakes +lamb +language +laptop +large +last +later +launching +lava +lawsuit +layout +lazy +lectures +ledge +leech +left +legion +leisure +lemon +lending +leopard +lesson +lettuce +lexicon +liar +library +licks +lids +lied +lifestyle +light +likewise +lilac +limits +linen +lion +lipstick +liquid +listen +lively +loaded +lobster +locker +lodge +lofty +logic +loincloth +long +looking +lopped +lordship +losing +lottery +loudly +love +lower +loyal +lucky +luggage +lukewarm +lullaby +lumber +lunar +lurk +lush +luxury +lymph +lynx +lyrics +macro +madness +magically +mailed +major +makeup +malady +mammal +maps +masterful +match +maul +maverick +maximum +mayor +maze +meant +mechanic +medicate +meeting +megabyte +melting +memoir +menu +merger +mesh +metro +mews +mice +midst +mighty +mime +mirror +misery +mittens +mixture +moat +mobile +mocked +mohawk +moisture +molten +moment +money +moon +mops +morsel +mostly +motherly +mouth +movement +mowing +much +muddy +muffin +mugged +mullet +mumble +mundane +muppet +mural +musical +muzzle +myriad +mystery +myth +nabbing +nagged +nail +names +nanny +napkin +narrate +nasty +natural +nautical +navy +nearby +necklace +needed +negative +neither +neon +nephew +nerves +nestle +network +neutral +never +newt +nexus +nibs +niche +niece +nifty +nightly +nimbly +nineteen +nirvana +nitrogen +nobody +nocturnal +nodes +noises +nomad +noodles +northern +nostril +noted +nouns +novelty +nowhere +nozzle +nuance +nucleus +nudged +nugget +nuisance +null +number +nuns +nurse +nutshell +nylon +oaks +oars +oasis +oatmeal +obedient +object +obliged +obnoxious +observant +obtains +obvious +occur +ocean +october +odds +odometer +offend +often +oilfield +ointment +okay +older +olive +olympics +omega +omission +omnibus +onboard +oncoming +oneself +ongoing +onion +online +onslaught +onto +onward +oozed +opacity +opened +opposite +optical +opus +orange +orbit +orchid +orders +organs +origin +ornament +orphans +oscar +ostrich +otherwise +otter +ouch +ought +ounce +ourselves +oust +outbreak +oval +oven +owed +owls +owner +oxidant +oxygen +oyster +ozone +pact +paddles +pager +pairing +palace +pamphlet +pancakes +paper +paradise +pastry +patio +pause +pavements +pawnshop +payment +peaches +pebbles +peculiar +pedantic +peeled +pegs +pelican +pencil +people +pepper +perfect +pests +petals +phase +pheasants +phone +phrases +physics +piano +picked +pierce +pigment +piloted +pimple +pinched +pioneer +pipeline +pirate +pistons +pitched +pivot +pixels +pizza +playful +pledge +pliers +plotting +plus +plywood +poaching +pockets +podcast +poetry +point +poker +polar +ponies +pool +popular +portents +possible +potato +pouch +poverty +powder +pram +present +pride +problems +pruned +prying +psychic +public +puck +puddle +puffin +pulp +pumpkins +punch +puppy +purged +push +putty +puzzled +pylons +pyramid +python +queen +quick +quote +rabbits +racetrack +radar +rafts +rage +railway +raking +rally +ramped +randomly +rapid +rarest +rash +rated +ravine +rays +razor +react +rebel +recipe +reduce +reef +refer +regular +reheat +reinvest +rejoices +rekindle +relic +remedy +renting +reorder +repent +request +reruns +rest +return +reunion +revamp +rewind +rhino +rhythm +ribbon +richly +ridges +rift +rigid +rims +ringing +riots +ripped +rising +ritual +river +roared +robot +rockets +rodent +rogue +roles +romance +roomy +roped +roster +rotate +rounded +rover +rowboat +royal +ruby +rudely +ruffled +rugged +ruined +ruling +rumble +runway +rural +rustled +ruthless +sabotage +sack +sadness +safety +saga +sailor +sake +salads +sample +sanity +sapling +sarcasm +sash +satin +saucepan +saved +sawmill +saxophone +sayings +scamper +scenic +school +science +scoop +scrub +scuba +seasons +second +sedan +seeded +segments +seismic +selfish +semifinal +sensible +september +sequence +serving +session +setup +seventh +sewage +shackles +shelter +shipped +shocking +shrugged +shuffled +shyness +siblings +sickness +sidekick +sieve +sifting +sighting +silk +simplest +sincerely +sipped +siren +situated +sixteen +sizes +skater +skew +skirting +skulls +skydive +slackens +sleepless +slid +slower +slug +smash +smelting +smidgen +smog +smuggled +snake +sneeze +sniff +snout +snug +soapy +sober +soccer +soda +software +soggy +soil +solved +somewhere +sonic +soothe +soprano +sorry +southern +sovereign +sowed +soya +space +speedy +sphere +spiders +splendid +spout +sprig +spud +spying +square +stacking +stellar +stick +stockpile +strained +stunning +stylishly +subtly +succeed +suddenly +suede +suffice +sugar +suitcase +sulking +summon +sunken +superior +surfer +sushi +suture +swagger +swept +swiftly +sword +swung +syllabus +symptoms +syndrome +syringe +system +taboo +tacit +tadpoles +tagged +tail +taken +talent +tamper +tanks +tapestry +tarnished +tasked +tattoo +taunts +tavern +tawny +taxi +teardrop +technical +tedious +teeming +tell +template +tender +tepid +tequila +terminal +testing +tether +textbook +thaw +theatrics +thirsty +thorn +threaten +thumbs +thwart +ticket +tidy +tiers +tiger +tilt +timber +tinted +tipsy +tirade +tissue +titans +toaster +tobacco +today +toenail +toffee +together +toilet +token +tolerant +tomorrow +tonic +toolbox +topic +torch +tossed +total +touchy +towel +toxic +toyed +trash +trendy +tribal +trolling +truth +trying +tsunami +tubes +tucks +tudor +tuesday +tufts +tugs +tuition +tulips +tumbling +tunnel +turnip +tusks +tutor +tuxedo +twang +tweezers +twice +twofold +tycoon +typist +tyrant +ugly +ulcers +ultimate +umbrella +umpire +unafraid +unbending +uncle +under +uneven +unfit +ungainly +unhappy +union +unjustly +unknown +unlikely +unmask +unnoticed +unopened +unplugs +unquoted +unrest +unsafe +until +unusual +unveil +unwind +unzip +upbeat +upcoming +update +upgrade +uphill +upkeep +upload +upon +upper +upright +upstairs +uptight +upwards +urban +urchins +urgent +usage +useful +usher +using +usual +utensils +utility +utmost +utopia +uttered +vacation +vague +vain +value +vampire +vane +vapidly +vary +vastness +vats +vaults +vector +veered +vegan +vehicle +vein +velvet +venomous +verification +vessel +veteran +vexed +vials +vibrate +victim +video +viewpoint +vigilant +viking +village +vinegar +violin +vipers +virtual +visited +vitals +vivid +vixen +vocal +vogue +voice +volcano +vortex +voted +voucher +vowels +voyage +vulture +wade +waffle +wagtail +waist +waking +wallets +wanted +warped +washing +water +waveform +waxing +wayside +weavers +website +wedge +weekday +weird +welders +went +wept +were +western +wetsuit +whale +when +whipped +whole +wickets +width +wield +wife +wiggle +wildly +winter +wipeout +wiring +wise +withdrawn +wives +wizard +wobbly +woes +woken +wolf +womanly +wonders +woozy +worry +wounded +woven +wrap +wrist +wrong +yacht +yahoo +yanks +yard +yawning +yearbook +yellow +yesterday +yeti +yields +yodel +yoga +younger +yoyo +zapped +zeal +zebra +zero +zesty +zigzags +zinger +zippers +zodiac +zombie +zones +zoom \ No newline at end of file diff --git a/package.json b/package.json index 1b0ff2902..90d4a8914 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ }, "dependencies": { "@appium/support": "^7.0.5", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", "@playwright/test": "^1.58.2", "@session-foundation/playwright-reporter": "^0.0.8", "@session-foundation/qa-seeder": "^0.1.26", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 165f2e352..0b1d65c0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,12 @@ importers: '@appium/support': specifier: ^7.0.5 version: 7.0.5 + '@noble/curves': + specifier: ^2.0.1 + version: 2.0.1 + '@noble/hashes': + specifier: ^2.0.1 + version: 2.0.1 '@playwright/test': specifier: ^1.58.2 version: 1.58.2 @@ -447,6 +453,14 @@ packages: '@jsquash/png@3.1.1': resolution: {integrity: sha512-C10pc+0H6j0h8fENOfnGOvkXCmvpSQTDGlfGd0sHphZhPSGTyLjIrHba0FaZZdsKqA/wlmhYicUHb92vfZphaw==} + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3400,6 +3414,12 @@ snapshots: '@jsquash/png@3.1.1': {} + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5627,7 +5647,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 diff --git a/run/constants/index.ts b/run/constants/index.ts index 1e9ba83a0..504d6b910 100644 --- a/run/constants/index.ts +++ b/run/constants/index.ts @@ -22,6 +22,8 @@ export const testLink = `https://getsession.org/`; export const DEVNET_URL = 'http://sesh-net.local:1280'; +export const PRO_BACKEND_URL = 'https://pro-backend-dev.getsession.org'; + export const ONS_MAPPINGS = { TESTQA: { ons: 'testqa', diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts new file mode 100644 index 000000000..e05a86fd0 --- /dev/null +++ b/run/test/utils/mock_pro.ts @@ -0,0 +1,394 @@ +/** + * Session Pro Test Account Setup + * + * Registers test accounts as Pro subscribers against the Session Pro dev backend, + * bypassing Google Play / Apple App Store verification entirely. + * + * Based on: + * https://github.com/session-foundation/session-pro-backend/blob/main/examples/endpoint_example.py + * + * Usage: + * import { makeAccountPro } from './mock_pro'; + * + * await makeAccountPro({ + * mnemonic: 'word1 word2 ... word13', + * provider: 'google' // or 'apple' + * }); + * + * In order for the changes to take effect in the clients it's best to force close and restart the app + */ + +import { ed25519 } from '@noble/curves/ed25519.js'; +import { blake2b } from '@noble/hashes/blake2.js'; +import { randomBytes } from 'crypto'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { PRO_BACKEND_URL } from '../../../constants'; + +export type PaymentProvider = 'apple' | 'google'; + +export interface MakeAccountProParams { + mnemonic: string; + provider: PaymentProvider; + dryRun?: boolean; // If true, build and print the request but don't send it +} + +export interface AddProPaymentRequest { + version: number; + master_pkey: string; + rotating_pkey: string; + master_sig: string; + rotating_sig: string; + payment_tx: { + provider: number; + google_payment_token?: string; + google_order_id?: string; + apple_tx_id?: string; + }; +} + +export interface ProProof { + version: number; + expiry_unix_ts_ms: number; + gen_index_hash: string; + rotating_pkey: string; + sig: string; +} + +export interface AddProPaymentResponse { + status: number; + result?: ProProof; + errors?: string[]; +} + +let WORDLIST_CACHE: string[] | null = null; + +function getWordlist(): string[] { + if (WORDLIST_CACHE) { + return WORDLIST_CACHE; + } + + const wordlistPath = join(__dirname, '../../../../english_wordlist.txt'); + const content = readFileSync(wordlistPath, 'utf-8'); + const words = content.split('\n').map(w => w.trim()).filter(Boolean); + + if (words.length !== 1626) { + throw new Error(`Expected 1626 words in wordlist, got ${words.length}`); + } + + WORDLIST_CACHE = words; + return words; +} + +// Decodes a 13-word recovery phrase a 16-byte seed hex string. */ +export function mnemonicToSeedHex(mnemonic: string): string { + const wordlist = getWordlist(); + const n = wordlist.length; // 1626 + + const words = mnemonic.toLowerCase().trim().split(/\s+/); + if (words.length !== 13) { + throw new Error(`Expected 13 words, got ${words.length}`); + } + + // Build word -> index lookup + const wordToIdx = new Map(); + wordlist.forEach((w, i) => wordToIdx.set(w, i)); + + // Resolve word indices (with prefix matching support) + const indices: number[] = []; + for (const word of words) { + if (wordToIdx.has(word)) { + indices.push(wordToIdx.get(word)!); + } else { + // Try prefix match (first 4 chars) + const matches = wordlist + .map((w, i) => ({ w, i })) + .filter(({ w }) => w.startsWith(word.slice(0, 4))); + + if (matches.length === 1) { + indices.push(matches[0].i); + } else { + throw new Error(`Unknown or ambiguous mnemonic word: '${word}'`); + } + } + } + + + // Decode: every 3 words -> 4 bytes (little-endian) + const dataIndices = indices.slice(0, 12); + const seedBytes: number[] = []; + for (let i = 0; i < 12; i += 3) { + const w1 = dataIndices[i]; + const w2 = dataIndices[i + 1]; + const w3 = dataIndices[i + 2]; + + const x = w1 + n * (((w2 - w1) % n + n) % n) + n * n * (((w3 - w2) % n + n) % n); + + // Convert to 4 bytes little-endian + seedBytes.push(x & 0xff); + seedBytes.push((x >> 8) & 0xff); + seedBytes.push((x >> 16) & 0xff); + seedBytes.push((x >> 24) & 0xff); + } + + if (seedBytes.length !== 16) { + throw new Error(`Expected 16 bytes, got ${seedBytes.length}`); + } + + return Buffer.from(seedBytes).toString('hex'); +} + +function padSeed(seedHex: string): Uint8Array { + const seed = Buffer.from(seedHex, 'hex'); + if (seed.length !== 16) { + throw new Error(`Seed must be 16 bytes, got ${seed.length}`); + } + + // Pad with 16 zero bytes + const padded = new Uint8Array(32); + padded.set(seed, 0); + return padded; +} + +// Derives the account-level Ed25519 keypair by zero-padding the 16-byte seed to 32 bytes. +export function deriveAccountEd25519Keypair(seedHex: string): { privateKey: Uint8Array; publicKey: Uint8Array } { + const padded = padSeed(seedHex); + const privateKey = padded; + const publicKey = ed25519.getPublicKey(privateKey); + return { privateKey, publicKey }; +} + +// Derives the Pro master keypair from the seed using Blake2b with "SessionProRandom" as the key. +export function deriveProMasterKey(seedHex: string): { privateKey: Uint8Array; publicKey: Uint8Array } { + const padded = padSeed(seedHex); + + // Blake2b-256 with "SessionProRandom" as the key + const proSeed = blake2b(padded, { + dkLen: 32, + key: Buffer.from('SessionProRandom', 'utf-8') + }); + + const privateKey = proSeed; + const publicKey = ed25519.getPublicKey(privateKey); + + return { privateKey, publicKey }; +} + +// Generates a random ephemeral rotating keypair for the payment request. +export function generateRotatingKey(): { privateKey: Uint8Array; publicKey: Uint8Array } { + const privateKey = ed25519.utils.randomSecretKey(); + const publicKey = ed25519.getPublicKey(privateKey); + return { privateKey, publicKey }; +} + + +function makeAddProPaymentHash( + version: number, + masterPubkey: Uint8Array, + rotatingPubkey: Uint8Array, + provider: number, + paymentToken?: string, + orderId?: string, + appleTxId?: string +): Uint8Array { + const personalization = Buffer.from('ProAddPayment___', 'utf-8'); // 16 bytes + + const parts: Uint8Array[] = [ + new Uint8Array([version]), + masterPubkey, + rotatingPubkey, + new Uint8Array([provider]) + ]; + + if (provider === 1) { // Google + if (!paymentToken || !orderId) { + throw new Error('Google provider requires payment_token and order_id'); + } + parts.push(Buffer.from(paymentToken, 'utf-8')); + parts.push(Buffer.from(orderId, 'utf-8')); + } else if (provider === 2) { // Apple + if (!appleTxId) { + throw new Error('Apple provider requires tx_id'); + } + parts.push(Buffer.from(appleTxId, 'utf-8')); + } + + // Concatenate all parts + const totalLen = parts.reduce((sum, p) => sum + p.length, 0); + const message = new Uint8Array(totalLen); + let offset = 0; + for (const part of parts) { + message.set(part, offset); + offset += part.length; + } + + return blake2b(message, { dkLen: 32, personalization }); +} + +// Builds a signed add_pro_payment request body with fake payment tokens. +export function buildAddProPaymentRequest( + masterKey: { privateKey: Uint8Array; publicKey: Uint8Array }, + rotatingKey: { privateKey: Uint8Array; publicKey: Uint8Array }, + provider: PaymentProvider +): AddProPaymentRequest { + const version = 0; + const providerNum = provider === 'google' ? 1 : 2; + + let paymentToken: string | undefined; + let orderId: string | undefined; + let appleTxId: string | undefined; + + const timestamp = Date.now(); + const nonce = randomBytes(4).toString('hex'); + + if (provider === 'google') { + paymentToken = `DEV.${timestamp}.${nonce}`; + orderId = `DEV.${timestamp}.${nonce}`; + } else { + appleTxId = `DEV.${timestamp}.${nonce}`; + } + + const hash = makeAddProPaymentHash( + version, + masterKey.publicKey, + rotatingKey.publicKey, + providerNum, + paymentToken, + orderId, + appleTxId + ); + + const masterSig = ed25519.sign(hash, masterKey.privateKey); + const rotatingSig = ed25519.sign(hash, rotatingKey.privateKey); + + const paymentTx: AddProPaymentRequest['payment_tx'] = { + provider: providerNum + }; + + if (provider === 'google') { + paymentTx.google_payment_token = paymentToken; + paymentTx.google_order_id = orderId; + } else { + paymentTx.apple_tx_id = appleTxId; + } + + return { + version, + master_pkey: Buffer.from(masterKey.publicKey).toString('hex'), + rotating_pkey: Buffer.from(rotatingKey.publicKey).toString('hex'), + master_sig: Buffer.from(masterSig).toString('hex'), + rotating_sig: Buffer.from(rotatingSig).toString('hex'), + payment_tx: paymentTx + }; +} + +// POSTs the payment request to the Pro backend with retries and timeout. +export async function addProPayment( + backendUrl: string, + request: AddProPaymentRequest, + { maxAttempts = 3, timeout = 10_000 } = {} +): Promise { + const url = `${backendUrl}/add_pro_payment`; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + const data = (await response.json()) as AddProPaymentResponse; + + if (!response.ok || data.status !== 0) { + throw new Error( + `Failed to add Pro payment: ${data.errors?.join(', ') || `HTTP ${response.status}`}` + ); + } + + return data; + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error'; + if (attempt === maxAttempts) { + // eslint-disable-next-line preserve-caught-error + throw new Error(`add_pro_payment failed after ${maxAttempts} attempts: ${msg}`); + } + console.log(`add_pro_payment attempt ${attempt}/${maxAttempts} failed: ${msg}, retrying...`); + } + } + + throw new Error('Unreachable'); +} + +// Registers a test account as a Pro subscriber against the dev backend. +export async function makeAccountPro(params: MakeAccountProParams): Promise { + const { mnemonic, provider, dryRun = false } = params; + + console.log('Deriving keys from mnemonic...'); + const seedHex = mnemonicToSeedHex(mnemonic); + + const masterKey = deriveProMasterKey(seedHex); + + console.log(` Master pubkey: ${Buffer.from(masterKey.publicKey).toString('hex')}`); + + // Generate rotating key + const rotatingKey = generateRotatingKey(); + console.log(` Rotating pubkey: ${Buffer.from(rotatingKey.publicKey).toString('hex')}`); + + // Build request + console.log(`\nBuilding add_pro_payment request (${provider})...`); + const request = buildAddProPaymentRequest(masterKey, rotatingKey, provider); + console.log('\nRequest body:'); + console.log(JSON.stringify(request, null, 2)); + + if (dryRun) { + console.log('\nDRY RUN - Request not sent'); + return null; + } + + // Send request + console.log(`\nSending request to ${PRO_BACKEND_URL}...`); + const response = await addProPayment(PRO_BACKEND_URL, request); + + if (!response.result) { + throw new Error('No proof in response'); + } + + console.log('Account successfully registered as Pro'); + console.log(` Expiry: ${new Date(response.result.expiry_unix_ts_ms).toISOString()}`); + + return response.result; +} + +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.length < 2) { + console.error('Usage: ts-node mock_pro.ts [--dry-run]'); + console.error('Example: ts-node mock_pro.ts "word1 word2 ..." google'); + console.error(' ts-node mock_pro.ts "word1 word2 ..." apple --dry-run'); + process.exit(1); + } + + const dryRun = args.includes('--dry-run'); + const filteredArgs = args.filter(a => a !== '--dry-run'); + const [mnemonic, provider] = filteredArgs; + + makeAccountPro({ + mnemonic, + provider: provider as PaymentProvider, + dryRun + }) + .then(() => process.exit(0)) + .catch(err => { + console.error('Error:', err.message); + process.exit(1); + }); +} \ No newline at end of file From 51116b3b767f2a288cbba04fe1be0f6a7b8d5c39 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 13:20:55 +1100 Subject: [PATCH 076/184] feat: start ios app in post pro mode --- run/test/utils/capabilities_ios.ts | 20 +++++++++++++++----- run/test/utils/open_app.ts | 6 ++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/run/test/utils/capabilities_ios.ts b/run/test/utils/capabilities_ios.ts index 2097e9207..6228b160c 100644 --- a/run/test/utils/capabilities_ios.ts +++ b/run/test/utils/capabilities_ios.ts @@ -7,6 +7,12 @@ import { IntRange } from '../../types/RangeType'; dotenv.config({ quiet: true }); +export type IOSTestContext = { + customInstallTime?: string; + sessionProEnabled?: string; +}; + + const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; export const iOSBundleId = 'com.loki-project.loki-messenger'; @@ -125,7 +131,7 @@ export function capabilityIsValid( export function getIosCapabilities( capabilitiesIndex: CapabilitiesIndexType, - customInstallTime?: string + customCaps?: IOSTestContext ): W3CXCUITestDriverCaps { if (capabilitiesIndex >= capabilities.length) { throw new Error( @@ -141,10 +147,14 @@ export function getIosCapabilities( const baseEnv = (caps['appium:processArguments'] as { env?: Record } | undefined)?.env ?? {}; - // Optional per-test override: - // Some tests set IOS_CUSTOM_FIRST_INSTALL_DATE_TIME before starting Appium. - // If present, inject it into the processArguments.env. Otherwise inject nothing. - const customEnv = customInstallTime ? { customFirstInstallDateTime: customInstallTime } : {}; + // Build custom env entries from per-test overrides + const customEnv: Record = {}; + if (customCaps?.customInstallTime) { + customEnv.customFirstInstallDateTime = customCaps.customInstallTime; + } + if (customCaps?.sessionProEnabled) { + customEnv.sessionPro = customCaps.sessionProEnabled; + } // Rebuild the processArguments block with merged env vars caps['appium:processArguments'] = { diff --git a/run/test/utils/open_app.ts b/run/test/utils/open_app.ts index 38655f111..bb6099e55 100644 --- a/run/test/utils/open_app.ts +++ b/run/test/utils/open_app.ts @@ -19,6 +19,7 @@ import { capabilityIsValid, getIosCapabilities, iOSBundleId, + IOSTestContext, } from './capabilities_ios'; import { cleanPermissions } from './permissions'; import { registerDevicesForTest } from './screenshot_helper'; @@ -29,9 +30,6 @@ const APPIUM_PORT = 4728; export type SupportedPlatformsType = 'android' | 'ios'; -export type IOSTestContext = { - customInstallTime?: string; -}; export const openAppMultipleDevices = async ( platform: SupportedPlatformsType, @@ -339,7 +337,7 @@ const openiOSApp = async ( const capabilities = getIosCapabilities( actualCapabilitiesIndex as CapabilitiesIndexType, - iOSContext?.customInstallTime + iOSContext ); const udid = capabilities.alwaysMatch['appium:udid'] as string; From 0113d754aaf0b5e10d517e0155e57c1e66f3e2b3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 13:21:37 +1100 Subject: [PATCH 077/184] feat: test animated display picture upload --- .gitattributes | 7 +- run/constants/testfiles.ts | 1 + run/test/locators/index.ts | 17 ++++- run/test/specs/cta_donate_review.spec.ts | 6 +- run/test/specs/cta_donate_time.spec.ts | 12 +--- .../specs/media/animated_profile_picture.webp | 3 + run/test/specs/message_length.spec.ts | 11 +-- ...r_actions_animated_profile_picture.spec.ts | 69 +++++++++++++++++++ run/test/utils/check_cta.ts | 38 ++++++++++ run/test/utils/test_setup.ts | 51 -------------- run/types/DeviceWrapper.ts | 65 +++++++++++------ run/types/testing.ts | 1 + 12 files changed, 182 insertions(+), 99 deletions(-) create mode 100644 run/test/specs/media/animated_profile_picture.webp create mode 100644 run/test/specs/user_actions_animated_profile_picture.spec.ts create mode 100644 run/test/utils/check_cta.ts delete mode 100644 run/test/utils/test_setup.ts diff --git a/.gitattributes b/.gitattributes index 99ac5c82c..64b88312a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ -run/screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/run/constants/testfiles.ts b/run/constants/testfiles.ts index 8f66e8cd7..751ca10ab 100644 --- a/run/constants/testfiles.ts +++ b/run/constants/testfiles.ts @@ -3,3 +3,4 @@ export const testFile = 'test_file.pdf'; export const testVideo = 'test_video.mp4'; export const testVideoThumbnail = 'test_video_thumbnail.png'; export const profilePicture = 'profile_picture.jpg'; +export const animatedProfilePicture = 'animated_profile_picture.webp' diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index bde576688..33ace6b1b 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -311,10 +311,25 @@ export class FirstGif extends LocatorsInterface { } } -export class ImageName extends LocatorsInterface { +export class GIFName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { // Dates can wildly differ between emulators but it will begin with "Photo taken on" on Android + case 'android': + return { + strategy: 'xpath', + selector: `//*[starts-with(@content-desc, "GIF taken on")]`, + }; + case 'ios': + throw new Error(`No such element on iOS`); + } + } +} + +export class ImageName extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + // Dates can wildly differ between emulators but it will begin with "GIF taken on" on Android case 'android': return { strategy: 'xpath', diff --git a/run/test/specs/cta_donate_review.spec.ts b/run/test/specs/cta_donate_review.spec.ts index 6000863d3..f34ba55ae 100644 --- a/run/test/specs/cta_donate_review.spec.ts +++ b/run/test/specs/cta_donate_review.spec.ts @@ -44,11 +44,7 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await forceStopAndRestartApp(device); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Donate CTA'), async () => { - await device.checkCTAStrings( - tStripped('donateSessionHelp'), - tStripped('donateSessionDescription'), - [tStripped('donate'), tStripped('maybeLater')] - ); + await device.checkCTA('donate'); }); // There *is* supposed to be a blur on Android but there is a bug on API 34 emulators preventing it from showing await test.step(TestSteps.VERIFY.SCREENSHOT('Donate CTA'), async () => { diff --git a/run/test/specs/cta_donate_time.spec.ts b/run/test/specs/cta_donate_time.spec.ts index 06844e0f0..04fed4468 100644 --- a/run/test/specs/cta_donate_time.spec.ts +++ b/run/test/specs/cta_donate_time.spec.ts @@ -1,15 +1,13 @@ import test, { TestInfo } from '@playwright/test'; -import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { iosIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { CTAButtonPositive } from '../locators/global'; import { PlusButton } from '../locators/home'; import { newUser } from '../utils/create_account'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { closeApp, - IOSTestContext, openAppOnPlatformSingleDevice, SupportedPlatformsType, } from '../utils/open_app'; @@ -42,11 +40,7 @@ async function donateCTAShowsSevenDaysAgo(platform: SupportedPlatformsType, test return { device }; }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Donate CTA'), async () => { - await device.checkCTAStrings( - tStripped('donateSessionHelp'), - tStripped('donateSessionDescription'), - [tStripped('donate'), tStripped('maybeLater')] - ); + await device.checkCTA('donate'); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); @@ -77,7 +71,7 @@ async function donateCTADoesntShowSixDaysAgo(platform: SupportedPlatformsType, t await test.step('Verify Donate CTA does not show', async () => { await Promise.all([ device.waitForTextElementToBePresent(new PlusButton(device)), - device.verifyElementNotPresent(new CTAButtonPositive(device)), + device.verifyNoCTAShows(), ]); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/media/animated_profile_picture.webp b/run/test/specs/media/animated_profile_picture.webp new file mode 100644 index 000000000..2c9906d2c --- /dev/null +++ b/run/test/specs/media/animated_profile_picture.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc5aad7aad38f0f279cfc0d5b8143dd8354420f505991ec49c0ee2fa97d099b +size 417482 diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 95e4fa386..7d720d9e3 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -97,16 +97,7 @@ for (const testCase of messageLengthTestCases) { // Android: CTA appears, verify and dismiss // Post-Pro is active on debug/qa builds by default // This will be the default for both platforms once Pro is live - await device.checkCTAStrings( - tStripped('upgradeTo'), - tStripped('proCallToActionLongerMessages'), - [tStripped('theContinue'), tStripped('cancel')], - [ - tStripped('proFeatureListLongerMessages'), - tStripped('proFeatureListPinnedConversations'), - tStripped('proFeatureListLoadsMore'), - ] - ); + await device.checkCTA('longerMessages'); await device.clickOnElementAll(new CTAButtonNegative(device)); await device.verifyElementNotPresent(new MessageBody(device, message)); } diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts new file mode 100644 index 000000000..17d1f332c --- /dev/null +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -0,0 +1,69 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { USERNAME } from '../../types/testing'; +import { PathMenuItem } from './locators/settings'; +import { newUser } from './utils/create_account'; +import { makeAccountPro } from './utils/mock_pro'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; +import { forceStopAndRestart } from './utils/utilities'; + +bothPlatformsIt({ + title: 'Upload animated profile picture (non Pro)', + risk: 'medium', + countOfDevicesNeeded: 1, + testCb: nonProAnimatedDP, + allureSuites: { + parent: 'User Actions', + suite: 'Change Profile Picture', + }, +}); + +bothPlatformsIt({ + title: 'Upload animated profile picture (Pro)', + risk: 'medium', + countOfDevicesNeeded: 1, + testCb: proAnimatedDP, + allureSuites: { + parent: 'User Actions', + suite: 'Change Profile Picture', + }, +}); + +async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await device.uploadProfilePicture(true); + await device.checkCTA('animatedProfilePicture'); + }); + + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} +async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); + await makeAccountPro({ + mnemonic: alice.recoveryPhrase, + provider: 'google' + }, +); + await forceStopAndRestart(device) + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await device.uploadProfilePicture(true); + }); + await device.waitForTextElementToBePresent(new PathMenuItem(device)) + await device.verifyNoCTAShows() + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} \ No newline at end of file diff --git a/run/test/utils/check_cta.ts b/run/test/utils/check_cta.ts new file mode 100644 index 000000000..41a7f8453 --- /dev/null +++ b/run/test/utils/check_cta.ts @@ -0,0 +1,38 @@ +import { tStripped } from '../../../localizer/lib'; + +export type CTAType = 'animatedProfilePicture' | 'donate' | 'longerMessages'; + +export type CTAConfig = { + heading: string; + body: string; + buttons: string[]; + features?: string[]; +} + +export const ctaConfigs: Record = { + donate: { + heading: tStripped('donateSessionHelp'), + body: tStripped('donateSessionDescription'), + buttons: [tStripped('donate'), tStripped('maybeLater')], + }, + longerMessages: { + heading: tStripped('upgradeTo'), + body: tStripped('proCallToActionLongerMessages'), + buttons: [tStripped('theContinue'), tStripped('cancel')], + features: [ + tStripped('proFeatureListLongerMessages'), + tStripped('proFeatureListPinnedConversations'), + tStripped('proFeatureListLoadsMore'), + ], + }, + animatedProfilePicture: { + heading: tStripped('upgradeTo'), + body: tStripped('proAnimatedDisplayPictureCallToActionDescription'), + buttons: [tStripped('theContinue'), tStripped('cancel')], + features: [ + tStripped('proFeatureListAnimatedDisplayPicture'), + tStripped('proFeatureListLongerMessages'), + tStripped('proFeatureListLoadsMore'), + ], + }, +}; diff --git a/run/test/utils/test_setup.ts b/run/test/utils/test_setup.ts deleted file mode 100644 index 97af4b9ed..000000000 --- a/run/test/utils/test_setup.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -Checkout branch that needs testing: -The Command needs to include: -- platform -- branch -- number of emulators ( new user to old user ratio ) - - -navigate to platform folder - Documents > session-(platform) -git checkout *branch* -then build from branch (ios does automatically, android requires manually click on hammer icon) - -ANDROID -start two emulators (cold boot on android) -cd ~/Library/Android/sdk -./emulator/emulator -avd Pixel_4_API_30 - -open new terminal window -cd ~/Library/Android/sdk -./emulator/emulator -avd Pixel_4_API_30_2 - -IOS -open -a simulator --args -IOS_1_SIMULATOR -no-boot-anim -open -a simulator --args -IOS_2_SIMULATOR -no-boot-anim - -run branch on emulator one -run branch on emulator two - -once session is running on emulator one: -click create session id -save session id -click continue -enter display name -continue -continue -continue -save recovery phrase from reminder -navigate out of reminder page - -once session is running on emulator two: -log into test account -click on restore account - - - - - - - -*/ diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 7ad9f1a1b..c1fdbc2d1 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -18,12 +18,14 @@ import { describeLocator, DownloadMediaButton, FirstGif, + GIFName, ImageName, ImagePermissionsModalAllow, LocatorsInterface, ReadReceiptsButton, } from '../../run/test/locators'; import { + animatedProfilePicture, profilePicture, testFile, testImage, @@ -68,7 +70,11 @@ import { UserSettings, VersionNumber, } from '../test/locators/settings'; -import { EnterAccountID, NewMessageOption, NextButton } from '../test/locators/start_conversation'; +import { + EnterAccountID, + NewMessageOption, + NextButton, +} from '../test/locators/start_conversation'; import { clickOnCoordinates, sleepFor } from '../test/utils'; import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; @@ -1874,7 +1880,7 @@ export class DeviceWrapper { } public async pushMediaToDevice( - mediaFileName: 'profile_picture.jpg' | 'test_file.pdf' | 'test_image.jpg' | 'test_video.mp4' + mediaFileName: 'animated_profile_picture.webp' | 'profile_picture.jpg' | 'test_file.pdf' | 'test_image.jpg' | 'test_video.mp4' ) { const filePath = path.join('run', 'test', 'specs', 'media', mediaFileName); if (this.isIOS()) { @@ -2155,7 +2161,17 @@ export class DeviceWrapper { return sentTimestamp; } - public async uploadProfilePicture() { + public async uploadProfilePicture(animated: boolean = false) { + let uploadPicture: 'animated_profile_picture.webp' | 'profile_picture.jpg' + let dpLocator + if (animated) { + uploadPicture = animatedProfilePicture + dpLocator = new GIFName(this) + } else { + uploadPicture = profilePicture + dpLocator = new ImageName(this) + } + await this.clickOnElementAll(new UserSettings(this)); // Click on Profile picture await this.clickOnElementAll(new UserAvatar(this)); @@ -2166,12 +2182,12 @@ export class DeviceWrapper { await sleepFor(5000); // sometimes Appium doesn't recognize the XPATH immediately await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, - profilePicture + uploadPicture ); await this.clickOnByAccessibilityID('Done'); } else if (this.isAndroid()) { // Push file first - await this.pushMediaToDevice(profilePicture); + await this.pushMediaToDevice(uploadPicture); await this.clickOnElementAll(new ImagePermissionsModalAllow(this)); await sleepFor(1000); await this.clickOnElementAll({ @@ -2179,8 +2195,8 @@ export class DeviceWrapper { selector: 'Image button', }); await sleepFor(500); - await this.clickOnElementAll(new ImageName(this)); - await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); + await this.clickOnElementAll(dpLocator); + if (!animated) { await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); } } await this.clickOnElementAll(new SaveProfilePictureButton(this)); } @@ -2523,21 +2539,12 @@ export class DeviceWrapper { this.assertTextMatches(actualDescription, expectedDescription, 'Modal description'); } - /** - * Checks CTA component text against expected values. - * CTAs contain: heading, body, 0-3 features, 1-2 buttons. - * @param heading - Expected CTA heading text - * @param body - Expected CTA body text - * @param buttons - Expected button text(s). First is positive, second (if present) is negative - * @param features - Optional array of expected feature text (0-3 items) - * @throws Error if any text element doesn't match expected value - */ - public async checkCTAStrings( - heading: string, - body: string, - buttons: string[], - features?: string[] - ): Promise { + private async checkCTAStrings({ + heading, + body, + buttons, + features, + }: CTAConfig): Promise { // Validate input if (features && features.length > 3) { throw new Error('CTAs support maximum 3 features'); @@ -2580,6 +2587,20 @@ export class DeviceWrapper { } } + public async checkCTA(type: CTAType): Promise { + await this.checkCTAStrings(ctaConfigs[type]); + } + + // This is the bare minimum of a CTA so we only check these + // Features may or may not exist anyway, same goes for negative buttons + public async verifyNoCTAShows(): Promise { + await Promise.all([ + this.verifyElementNotPresent(new CTAHeading(this)), + this.verifyElementNotPresent(new CTABody(this)), + this.verifyElementNotPresent(new CTAButtonPositive(this)) + ]) + } + public async getElementPixelColor(args: LocatorsInterface): Promise { // Wait for the element to be present const element = await this.waitForTextElementToBePresent(args); diff --git a/run/types/testing.ts b/run/types/testing.ts index e8032a018..b02be61ac 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -124,6 +124,7 @@ export type XPath = | `(//XCUIElementTypeImage[@name="gif cell"])[1]` | `//*[./*[@name='${DISAPPEARING_TIMES}']]/*[2]` | `//*[@resource-id='network.loki.messenger:id/callTitle' and contains(@text, ':')]` + | `//*[starts-with(@content-desc, "GIF taken on")]` | `//*[starts-with(@content-desc, "Photo taken on")]` | `//android.view.ViewGroup[@resource-id='network.loki.messenger:id/mainContainer'][.//android.widget.TextView[contains(@text,'${string}')]]//androidx.compose.ui.platform.ComposeView[@resource-id='network.loki.messenger:id/profilePictureView']` | `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger:id/layout_emoji_container"]` From 6e32b55fc5ed2f60cc57c2d0228b14700d9ed490 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Feb 2026 17:04:36 +1100 Subject: [PATCH 078/184] fix: use rainbow gif, sample pixels for animation --- run/constants/testfiles.ts | 2 +- .../specs/media/animated_profile_picture.gif | 3 + .../specs/media/animated_profile_picture.webp | 3 - ...r_actions_animated_profile_picture.spec.ts | 20 ++-- run/test/utils/capabilities_ios.ts | 1 - run/test/utils/check_cta.ts | 2 +- run/test/utils/mock_pro.ts | 113 ++++++++++-------- run/test/utils/open_app.ts | 1 - run/types/DeviceWrapper.ts | 56 ++++++--- 9 files changed, 113 insertions(+), 88 deletions(-) create mode 100644 run/test/specs/media/animated_profile_picture.gif delete mode 100644 run/test/specs/media/animated_profile_picture.webp diff --git a/run/constants/testfiles.ts b/run/constants/testfiles.ts index 751ca10ab..ba5d230b6 100644 --- a/run/constants/testfiles.ts +++ b/run/constants/testfiles.ts @@ -3,4 +3,4 @@ export const testFile = 'test_file.pdf'; export const testVideo = 'test_video.mp4'; export const testVideoThumbnail = 'test_video_thumbnail.png'; export const profilePicture = 'profile_picture.jpg'; -export const animatedProfilePicture = 'animated_profile_picture.webp' +export const animatedProfilePicture = 'animated_profile_picture.gif'; diff --git a/run/test/specs/media/animated_profile_picture.gif b/run/test/specs/media/animated_profile_picture.gif new file mode 100644 index 000000000..e609338d1 --- /dev/null +++ b/run/test/specs/media/animated_profile_picture.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cbfc07234028c655e51d9a3d218061c2363b314b4ce59101ce0bfac6e085d77 +size 33041 diff --git a/run/test/specs/media/animated_profile_picture.webp b/run/test/specs/media/animated_profile_picture.webp deleted file mode 100644 index 2c9906d2c..000000000 --- a/run/test/specs/media/animated_profile_picture.webp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cdc5aad7aad38f0f279cfc0d5b8143dd8354420f505991ec49c0ee2fa97d099b -size 417482 diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 17d1f332c..f3de9f41b 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { PathMenuItem } from './locators/settings'; +import { PathMenuItem, UserAvatar } from './locators/settings'; import { newUser } from './utils/create_account'; import { makeAccountPro } from './utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; @@ -52,18 +52,18 @@ async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInf const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); - await makeAccountPro({ - mnemonic: alice.recoveryPhrase, - provider: 'google' - }, -); - await forceStopAndRestart(device) + await makeAccountPro({ + mnemonic: alice.recoveryPhrase, + provider: 'google', + }); + await forceStopAndRestart(device); await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { await device.uploadProfilePicture(true); }); - await device.waitForTextElementToBePresent(new PathMenuItem(device)) - await device.verifyNoCTAShows() + await device.waitForTextElementToBePresent(new PathMenuItem(device)); + await device.verifyNoCTAShows(); + await device.verifyElementIsAnimated(new UserAvatar(device)); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); }); -} \ No newline at end of file +} diff --git a/run/test/utils/capabilities_ios.ts b/run/test/utils/capabilities_ios.ts index 6228b160c..aca51480b 100644 --- a/run/test/utils/capabilities_ios.ts +++ b/run/test/utils/capabilities_ios.ts @@ -12,7 +12,6 @@ export type IOSTestContext = { sessionProEnabled?: string; }; - const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; export const iOSBundleId = 'com.loki-project.loki-messenger'; diff --git a/run/test/utils/check_cta.ts b/run/test/utils/check_cta.ts index 41a7f8453..a7f9cd7d4 100644 --- a/run/test/utils/check_cta.ts +++ b/run/test/utils/check_cta.ts @@ -7,7 +7,7 @@ export type CTAConfig = { body: string; buttons: string[]; features?: string[]; -} +}; export const ctaConfigs: Record = { donate: { diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index e05a86fd0..06e68a858 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -1,20 +1,20 @@ /** * Session Pro Test Account Setup - * + * * Registers test accounts as Pro subscribers against the Session Pro dev backend, * bypassing Google Play / Apple App Store verification entirely. - * - * Based on: + * + * Based on: * https://github.com/session-foundation/session-pro-backend/blob/main/examples/endpoint_example.py - * + * * Usage: * import { makeAccountPro } from './mock_pro'; - * + * * await makeAccountPro({ * mnemonic: 'word1 word2 ... word13', * provider: 'google' // or 'apple' * }); - * + * * In order for the changes to take effect in the clients it's best to force close and restart the app */ @@ -31,7 +31,7 @@ export type PaymentProvider = 'apple' | 'google'; export interface MakeAccountProParams { mnemonic: string; provider: PaymentProvider; - dryRun?: boolean; // If true, build and print the request but don't send it + dryRun?: boolean; // If true, build and print the request but don't send it } export interface AddProPaymentRequest { @@ -71,7 +71,10 @@ function getWordlist(): string[] { const wordlistPath = join(__dirname, '../../../../english_wordlist.txt'); const content = readFileSync(wordlistPath, 'utf-8'); - const words = content.split('\n').map(w => w.trim()).filter(Boolean); + const words = content + .split('\n') + .map(w => w.trim()) + .filter(Boolean); if (words.length !== 1626) { throw new Error(`Expected 1626 words in wordlist, got ${words.length}`); @@ -105,7 +108,7 @@ export function mnemonicToSeedHex(mnemonic: string): string { const matches = wordlist .map((w, i) => ({ w, i })) .filter(({ w }) => w.startsWith(word.slice(0, 4))); - + if (matches.length === 1) { indices.push(matches[0].i); } else { @@ -114,7 +117,6 @@ export function mnemonicToSeedHex(mnemonic: string): string { } } - // Decode: every 3 words -> 4 bytes (little-endian) const dataIndices = indices.slice(0, 12); const seedBytes: number[] = []; @@ -122,9 +124,9 @@ export function mnemonicToSeedHex(mnemonic: string): string { const w1 = dataIndices[i]; const w2 = dataIndices[i + 1]; const w3 = dataIndices[i + 2]; - - const x = w1 + n * (((w2 - w1) % n + n) % n) + n * n * (((w3 - w2) % n + n) % n); - + + const x = w1 + n * ((((w2 - w1) % n) + n) % n) + n * n * ((((w3 - w2) % n) + n) % n); + // Convert to 4 bytes little-endian seedBytes.push(x & 0xff); seedBytes.push((x >> 8) & 0xff); @@ -144,7 +146,7 @@ function padSeed(seedHex: string): Uint8Array { if (seed.length !== 16) { throw new Error(`Seed must be 16 bytes, got ${seed.length}`); } - + // Pad with 16 zero bytes const padded = new Uint8Array(32); padded.set(seed, 0); @@ -152,37 +154,42 @@ function padSeed(seedHex: string): Uint8Array { } // Derives the account-level Ed25519 keypair by zero-padding the 16-byte seed to 32 bytes. -export function deriveAccountEd25519Keypair(seedHex: string): { privateKey: Uint8Array; publicKey: Uint8Array } { +export function deriveAccountEd25519Keypair(seedHex: string): { + privateKey: Uint8Array; + publicKey: Uint8Array; +} { const padded = padSeed(seedHex); const privateKey = padded; const publicKey = ed25519.getPublicKey(privateKey); return { privateKey, publicKey }; } -// Derives the Pro master keypair from the seed using Blake2b with "SessionProRandom" as the key. -export function deriveProMasterKey(seedHex: string): { privateKey: Uint8Array; publicKey: Uint8Array } { +// Derives the Pro master keypair from the seed using Blake2b with "SessionProRandom" as the key. +export function deriveProMasterKey(seedHex: string): { + privateKey: Uint8Array; + publicKey: Uint8Array; +} { const padded = padSeed(seedHex); - + // Blake2b-256 with "SessionProRandom" as the key - const proSeed = blake2b(padded, { + const proSeed = blake2b(padded, { dkLen: 32, - key: Buffer.from('SessionProRandom', 'utf-8') + key: Buffer.from('SessionProRandom', 'utf-8'), }); - + const privateKey = proSeed; const publicKey = ed25519.getPublicKey(privateKey); - + return { privateKey, publicKey }; } -// Generates a random ephemeral rotating keypair for the payment request. +// Generates a random ephemeral rotating keypair for the payment request. export function generateRotatingKey(): { privateKey: Uint8Array; publicKey: Uint8Array } { const privateKey = ed25519.utils.randomSecretKey(); const publicKey = ed25519.getPublicKey(privateKey); return { privateKey, publicKey }; } - function makeAddProPaymentHash( version: number, masterPubkey: Uint8Array, @@ -193,27 +200,29 @@ function makeAddProPaymentHash( appleTxId?: string ): Uint8Array { const personalization = Buffer.from('ProAddPayment___', 'utf-8'); // 16 bytes - + const parts: Uint8Array[] = [ new Uint8Array([version]), masterPubkey, rotatingPubkey, - new Uint8Array([provider]) + new Uint8Array([provider]), ]; - - if (provider === 1) { // Google + + if (provider === 1) { + // Google if (!paymentToken || !orderId) { throw new Error('Google provider requires payment_token and order_id'); } parts.push(Buffer.from(paymentToken, 'utf-8')); parts.push(Buffer.from(orderId, 'utf-8')); - } else if (provider === 2) { // Apple + } else if (provider === 2) { + // Apple if (!appleTxId) { throw new Error('Apple provider requires tx_id'); } parts.push(Buffer.from(appleTxId, 'utf-8')); } - + // Concatenate all parts const totalLen = parts.reduce((sum, p) => sum + p.length, 0); const message = new Uint8Array(totalLen); @@ -222,7 +231,7 @@ function makeAddProPaymentHash( message.set(part, offset); offset += part.length; } - + return blake2b(message, { dkLen: 32, personalization }); } @@ -234,11 +243,11 @@ export function buildAddProPaymentRequest( ): AddProPaymentRequest { const version = 0; const providerNum = provider === 'google' ? 1 : 2; - + let paymentToken: string | undefined; let orderId: string | undefined; let appleTxId: string | undefined; - + const timestamp = Date.now(); const nonce = randomBytes(4).toString('hex'); @@ -248,7 +257,7 @@ export function buildAddProPaymentRequest( } else { appleTxId = `DEV.${timestamp}.${nonce}`; } - + const hash = makeAddProPaymentHash( version, masterKey.publicKey, @@ -258,28 +267,28 @@ export function buildAddProPaymentRequest( orderId, appleTxId ); - + const masterSig = ed25519.sign(hash, masterKey.privateKey); const rotatingSig = ed25519.sign(hash, rotatingKey.privateKey); - + const paymentTx: AddProPaymentRequest['payment_tx'] = { - provider: providerNum + provider: providerNum, }; - + if (provider === 'google') { paymentTx.google_payment_token = paymentToken; paymentTx.google_order_id = orderId; } else { paymentTx.apple_tx_id = appleTxId; } - + return { version, master_pkey: Buffer.from(masterKey.publicKey).toString('hex'), rotating_pkey: Buffer.from(rotatingKey.publicKey).toString('hex'), master_sig: Buffer.from(masterSig).toString('hex'), rotating_sig: Buffer.from(rotatingSig).toString('hex'), - payment_tx: paymentTx + payment_tx: paymentTx, }; } @@ -327,7 +336,7 @@ export async function addProPayment( throw new Error('Unreachable'); } -// Registers a test account as a Pro subscriber against the dev backend. +// Registers a test account as a Pro subscriber against the dev backend. export async function makeAccountPro(params: MakeAccountProParams): Promise { const { mnemonic, provider, dryRun = false } = params; @@ -335,41 +344,41 @@ export async function makeAccountPro(params: MakeAccountProParams): Promise [--dry-run]'); console.error('Example: ts-node mock_pro.ts "word1 word2 ..." google'); @@ -384,11 +393,11 @@ if (require.main === module) { makeAccountPro({ mnemonic, provider: provider as PaymentProvider, - dryRun + dryRun, }) .then(() => process.exit(0)) .catch(err => { console.error('Error:', err.message); process.exit(1); }); -} \ No newline at end of file +} diff --git a/run/test/utils/open_app.ts b/run/test/utils/open_app.ts index bb6099e55..e3640b58c 100644 --- a/run/test/utils/open_app.ts +++ b/run/test/utils/open_app.ts @@ -30,7 +30,6 @@ const APPIUM_PORT = 4728; export type SupportedPlatformsType = 'android' | 'ios'; - export const openAppMultipleDevices = async ( platform: SupportedPlatformsType, numberOfDevices: number, diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index c1fdbc2d1..84a325199 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1,7 +1,7 @@ import type { Constraints, DefaultCreateSessionResult } from '@appium/types'; import { getImageOccurrence } from '@appium/opencv'; -import { TestInfo } from '@playwright/test'; +import { expect, TestInfo } from '@playwright/test'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; import { W3CUiautomator2DriverCaps } from 'appium-uiautomator2-driver/build/lib/types'; import { W3CXCUITestDriverCaps, XCUITestDriver } from 'appium-xcuitest-driver/build/lib/driver'; @@ -1880,7 +1880,12 @@ export class DeviceWrapper { } public async pushMediaToDevice( - mediaFileName: 'animated_profile_picture.webp' | 'profile_picture.jpg' | 'test_file.pdf' | 'test_image.jpg' | 'test_video.mp4' + mediaFileName: + | 'animated_profile_picture.gif' + | 'profile_picture.jpg' + | 'test_file.pdf' + | 'test_image.jpg' + | 'test_video.mp4' ) { const filePath = path.join('run', 'test', 'specs', 'media', mediaFileName); if (this.isIOS()) { @@ -2162,14 +2167,14 @@ export class DeviceWrapper { } public async uploadProfilePicture(animated: boolean = false) { - let uploadPicture: 'animated_profile_picture.webp' | 'profile_picture.jpg' - let dpLocator + let uploadPicture: 'animated_profile_picture.gif' | 'profile_picture.jpg'; + let dpLocator; if (animated) { - uploadPicture = animatedProfilePicture - dpLocator = new GIFName(this) + uploadPicture = animatedProfilePicture; + dpLocator = new GIFName(this); } else { - uploadPicture = profilePicture - dpLocator = new ImageName(this) + uploadPicture = profilePicture; + dpLocator = new ImageName(this); } await this.clickOnElementAll(new UserSettings(this)); @@ -2196,7 +2201,9 @@ export class DeviceWrapper { }); await sleepFor(500); await this.clickOnElementAll(dpLocator); - if (!animated) { await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); } + if (!animated) { + await this.clickOnElementById('network.loki.messenger:id/crop_image_menu_crop'); + } } await this.clickOnElementAll(new SaveProfilePictureButton(this)); } @@ -2539,12 +2546,7 @@ export class DeviceWrapper { this.assertTextMatches(actualDescription, expectedDescription, 'Modal description'); } - private async checkCTAStrings({ - heading, - body, - buttons, - features, - }: CTAConfig): Promise { + private async checkCTAStrings({ heading, body, buttons, features }: CTAConfig): Promise { // Validate input if (features && features.length > 3) { throw new Error('CTAs support maximum 3 features'); @@ -2591,14 +2593,14 @@ export class DeviceWrapper { await this.checkCTAStrings(ctaConfigs[type]); } - // This is the bare minimum of a CTA so we only check these - // Features may or may not exist anyway, same goes for negative buttons + // This is the bare minimum of a CTA so we only check these + // Features may or may not exist anyway, same goes for negative buttons public async verifyNoCTAShows(): Promise { await Promise.all([ this.verifyElementNotPresent(new CTAHeading(this)), this.verifyElementNotPresent(new CTABody(this)), - this.verifyElementNotPresent(new CTAButtonPositive(this)) - ]) + this.verifyElementNotPresent(new CTAButtonPositive(this)), + ]); } public async getElementPixelColor(args: LocatorsInterface): Promise { @@ -2609,6 +2611,22 @@ export class DeviceWrapper { const pixelColor = await parseDataImage(base64image); return pixelColor; } + // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. + // If the set contains more than 1 color it is likely animated. + public async verifyElementIsAnimated(args: LocatorsInterface): Promise { + const SAMPLE_SIZE = 3; + const colors = new Set(); + for (let i = 0; i < SAMPLE_SIZE; i++) { + colors.add(await this.getElementPixelColor(args)); + if (i < SAMPLE_SIZE - 1) { + await sleepFor(300); + } + } + expect( + colors.size, + `Expected element to be animated but detected 1 unique color: ${[...colors][0]}` + ).toBeGreaterThan(1); + } public async getVersionNumber() { // NOTE if this becomes necessary for more tests, consider adding a property/caching to the DeviceWrapper From c22f4cb5efef93c08c0d47073dc99c0cc191dbc7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 09:07:58 +1100 Subject: [PATCH 079/184] fix: recovery banner only shows with >2 convos --- run/test/locators/settings.ts | 4 ++-- run/test/utils/create_account.ts | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index 0352e10e5..f9fa622f8 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -208,8 +208,8 @@ export class RecoveryPasswordMenuItem extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'id', - selector: 'Recovery password menu item', + strategy: '-android uiautomator', + selector: 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Recovery password menu item"))', } as const; case 'ios': return { diff --git a/run/test/utils/create_account.ts b/run/test/utils/create_account.ts index 05922ee2e..22dbfe563 100644 --- a/run/test/utils/create_account.ts +++ b/run/test/utils/create_account.ts @@ -10,9 +10,8 @@ import { FastModeRadio, SlowModeRadio, } from '../locators/onboarding'; -import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; +import { RecoveryPasswordMenuItem, RecoveryPhraseContainer } from '../locators/settings'; import { UserSettings } from '../locators/settings'; -import { CopyButton } from '../locators/start_conversation'; import { handleBackgroundPermissions, handleNotificationPermissions } from './permissions'; export type BaseSetupOptions = { @@ -70,18 +69,17 @@ export async function newUser( } // Open recovery phrase modal and save recovery phrase - await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); - await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + await device.clickOnElementAll(new UserSettings(device)); + await device.onIOS().scrollDown(); + await device.clickOnElementAll(new RecoveryPasswordMenuItem(device)); const recoveryPhraseContainer = await device.clickOnElementAll( new RecoveryPhraseContainer(device) ); - await device.onAndroid().clickOnElementAll(new CopyButton(device)); const recoveryPhrase = await device.getTextFromElement(recoveryPhraseContainer); device.log(`${userName}s recovery phrase is "${recoveryPhrase}"`); await device.navigateBack(false); - + await device.scrollUp(); // Get Account ID from User Settings - await device.clickOnElementAll(new UserSettings(device)); const el = await device.waitForTextElementToBePresent(new AccountIDDisplay(device)); const accountID = await device.getTextFromElement(el); await device.clickOnElementAll(new CloseSettings(device)); From f052e5ba97f6d8f5f008cd0459f2d6fddee27ce3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 10:19:24 +1100 Subject: [PATCH 080/184] fix: android 1.32.0 locator and flow changes --- run/test/locators/index.ts | 7 +++---- run/test/specs/review_positive.spec.ts | 9 ++------- run/types/DeviceWrapper.ts | 4 ---- run/types/testing.ts | 4 ++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index 33ace6b1b..f59f39793 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -51,8 +51,8 @@ export class BlockedContactsSettings extends LocatorsInterface { switch (this.platform) { case 'android': return { - strategy: 'accessibility id', - selector: 'qa-blocked-contacts-settings-item', + strategy: 'id', + selector: 'preferences-option-blocked-contacts', }; case 'ios': return { @@ -471,8 +471,7 @@ export class ReadReceiptsButton extends LocatorsInterface { case 'android': return { strategy: 'id', - selector: 'android:id/summary', - text: 'Show read receipts for all messages you send and receive.', + selector: 'preferences-option-read-receipt', } as const; case 'ios': return { diff --git a/run/test/specs/review_positive.spec.ts b/run/test/specs/review_positive.spec.ts index 86a8ff268..b3344db27 100644 --- a/run/test/specs/review_positive.spec.ts +++ b/run/test/specs/review_positive.spec.ts @@ -28,11 +28,6 @@ bothPlatformsIt({ async function reviewPromptPositive(platform: SupportedPlatformsType, testInfo: TestInfo) { const storevariant = platform === 'android' ? 'Google Play Store' : 'App Store'; - // Platform specific string for the Rate Session modal - const rateModalDescriptionString = - platform === 'android' - ? tStripped('rateSessionModalDescription', { storevariant }) - : tStripped('rateSessionModalDescriptionUpdated', { storevariant }); const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); @@ -52,9 +47,9 @@ async function reviewPromptPositive(platform: SupportedPlatformsType, testInfo: await device.clickOnElementAll(new ReviewPromptItsGreatButton(device)); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Rate Session'), async () => { - await device.checkModalStrings(tStripped('rateSession'), rateModalDescriptionString); + await device.checkModalStrings(tStripped('rateSession'), tStripped('rateSessionModalDescriptionUpdated', { storevariant })); await device.waitForTextElementToBePresent(new ReviewPromptRateAppButton(device)); - await device.onAndroid().waitForTextElementToBePresent(new ReviewPromptNotNowButton(device)); // On iOS the modal only has the Rate button + await device.verifyElementNotPresent(new ReviewPromptNotNowButton(device)); // This modal now only has the Rate button }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 84a325199..ef1c067d3 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2386,14 +2386,10 @@ export class DeviceWrapper { public async turnOnReadReceipts() { await this.navigateBack(); - await sleepFor(100); await this.clickOnElementAll(new UserSettings(this)); - await sleepFor(500); await this.clickOnElementAll(new PrivacyMenuItem(this)); - await sleepFor(2000); await this.clickOnElementAll(new ReadReceiptsButton(this)); await this.navigateBack(false); - await sleepFor(100); await this.clickOnElementAll(new CloseSettings(this)); } diff --git a/run/types/testing.ts b/run/types/testing.ts index b02be61ac..f547a2efa 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -358,7 +358,6 @@ export type AccessibilityId = | 'Pin' | 'Please enter a shorter group name' | 'Privacy Policy' - | 'qa-blocked-contacts-settings-item' | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' @@ -436,7 +435,6 @@ export type Id = | 'android:id/alertTitle' | 'android:id/button1' | 'android:id/content_preview_text' - | 'android:id/summary' | 'android:id/title' | 'android.widget.TextView' | 'Appearance' @@ -567,6 +565,8 @@ export type Id = | 'open-survey-button' | 'Open' | 'Open URL' + | 'preferences-option-blocked-contacts' + | 'preferences-option-read-receipt' | 'preferred-display-name' | 'Privacy' | 'Privacy policy button' From 954dbe6c9c35c91b616ae25576e55ec686081d25 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 13:40:37 +1100 Subject: [PATCH 081/184] chore: more restructure/merge errors --- eslint.config.mjs | 1 + run/test/locators/settings.ts | 3 ++- run/test/specs/app_disguise_set.spec.ts | 7 ++++++- run/test/specs/cta_donate_time.spec.ts | 8 ++------ run/test/specs/review_positive.spec.ts | 5 ++++- .../user_actions_animated_profile_picture.spec.ts | 10 +++++----- run/test/utils/check_cta.ts | 2 +- run/test/utils/mock_pro.ts | 3 +-- run/types/DeviceWrapper.ts | 11 ++++------- 9 files changed, 26 insertions(+), 24 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9c4e4dd0f..19027f038 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default defineConfig( rules: { 'no-unused-vars': 'off', // we have @typescript-eslint/no-unused-vars enabled below 'no-else-return': 'error', + 'preserve-caught-error': 'off', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-explicit-any': 'off', diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index f9fa622f8..7761885bd 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -209,7 +209,8 @@ export class RecoveryPasswordMenuItem extends LocatorsInterface { case 'android': return { strategy: '-android uiautomator', - selector: 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Recovery password menu item"))', + selector: + 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("Recovery password menu item"))', } as const; case 'ios': return { diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index f9a32af50..c0abcbf5a 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -14,7 +14,12 @@ import { } from '../locators/settings'; import { sleepFor } from '../utils'; import { newUser } from '../utils/create_account'; -import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType, uninstallApp } from '../utils/open_app'; +import { + closeApp, + openAppOnPlatformSingleDevice, + SupportedPlatformsType, + uninstallApp, +} from '../utils/open_app'; bothPlatformsItSeparate({ title: 'App disguise set icon', diff --git a/run/test/specs/cta_donate_time.spec.ts b/run/test/specs/cta_donate_time.spec.ts index 04fed4468..c7e1f46c2 100644 --- a/run/test/specs/cta_donate_time.spec.ts +++ b/run/test/specs/cta_donate_time.spec.ts @@ -4,13 +4,9 @@ import { TestSteps } from '../../types/allure'; import { iosIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { PlusButton } from '../locators/home'; -import { newUser } from '../utils/create_account'; import { IOSTestContext } from '../utils/capabilities_ios'; -import { - closeApp, - openAppOnPlatformSingleDevice, - SupportedPlatformsType, -} from '../utils/open_app'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; import { setIOSFirstInstallDate } from '../utils/time_travel'; // iOS uses app-level time override (customFirstInstallDateTime capability). diff --git a/run/test/specs/review_positive.spec.ts b/run/test/specs/review_positive.spec.ts index b3344db27..8de5d6491 100644 --- a/run/test/specs/review_positive.spec.ts +++ b/run/test/specs/review_positive.spec.ts @@ -47,7 +47,10 @@ async function reviewPromptPositive(platform: SupportedPlatformsType, testInfo: await device.clickOnElementAll(new ReviewPromptItsGreatButton(device)); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Rate Session'), async () => { - await device.checkModalStrings(tStripped('rateSession'), tStripped('rateSessionModalDescriptionUpdated', { storevariant })); + await device.checkModalStrings( + tStripped('rateSession'), + tStripped('rateSessionModalDescriptionUpdated', { storevariant }) + ); await device.waitForTextElementToBePresent(new ReviewPromptRateAppButton(device)); await device.verifyElementNotPresent(new ReviewPromptNotNowButton(device)); // This modal now only has the Rate button }); diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index f3de9f41b..6f90f770c 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -3,11 +3,11 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { PathMenuItem, UserAvatar } from './locators/settings'; -import { newUser } from './utils/create_account'; -import { makeAccountPro } from './utils/mock_pro'; -import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from './utils/open_app'; -import { forceStopAndRestart } from './utils/utilities'; +import { PathMenuItem, UserAvatar } from '../locators/settings'; +import { newUser } from '../utils/create_account'; +import { makeAccountPro } from '../utils/mock_pro'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { forceStopAndRestart } from '../utils/utilities'; bothPlatformsIt({ title: 'Upload animated profile picture (non Pro)', diff --git a/run/test/utils/check_cta.ts b/run/test/utils/check_cta.ts index a7f9cd7d4..96cc65665 100644 --- a/run/test/utils/check_cta.ts +++ b/run/test/utils/check_cta.ts @@ -1,4 +1,4 @@ -import { tStripped } from '../../../localizer/lib'; +import { tStripped } from '../../localizer/lib'; export type CTAType = 'animatedProfilePicture' | 'donate' | 'longerMessages'; diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index 06e68a858..32e8b1261 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -24,7 +24,7 @@ import { randomBytes } from 'crypto'; import { readFileSync } from 'fs'; import { join } from 'path'; -import { PRO_BACKEND_URL } from '../../../constants'; +import { PRO_BACKEND_URL } from '../../constants'; export type PaymentProvider = 'apple' | 'google'; @@ -326,7 +326,6 @@ export async function addProPayment( } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; if (attempt === maxAttempts) { - // eslint-disable-next-line preserve-caught-error throw new Error(`add_pro_payment failed after ${maxAttempts} attempts: ${msg}`); } console.log(`add_pro_payment attempt ${attempt}/${maxAttempts} failed: ${msg}, retrying...`); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index ef1c067d3..5ae88bc04 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -70,15 +70,12 @@ import { UserSettings, VersionNumber, } from '../test/locators/settings'; -import { - EnterAccountID, - NewMessageOption, - NextButton, -} from '../test/locators/start_conversation'; +import { EnterAccountID, NewMessageOption, NextButton } from '../test/locators/start_conversation'; import { clickOnCoordinates, sleepFor } from '../test/utils'; import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; +import { CTAConfig, ctaConfigs, CTAType } from '../test/utils/check_cta'; import { SupportedPlatformsType } from '../test/utils/open_app'; import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; import { @@ -2607,8 +2604,8 @@ export class DeviceWrapper { const pixelColor = await parseDataImage(base64image); return pixelColor; } - // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. - // If the set contains more than 1 color it is likely animated. + // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. + // If the set contains more than 1 color it is likely animated. public async verifyElementIsAnimated(args: LocatorsInterface): Promise { const SAMPLE_SIZE = 3; const colors = new Set(); From 4a782cab9aed173f9a489880ba130de3f3795b22 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 13:57:45 +1100 Subject: [PATCH 082/184] fix: push files from correct location --- eslint.config.mjs | 1 + run/types/DeviceWrapper.ts | 10 ++++++++-- scripts/create_ios_simulators.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9c4e4dd0f..19027f038 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default defineConfig( rules: { 'no-unused-vars': 'off', // we have @typescript-eslint/no-unused-vars enabled below 'no-else-return': 'error', + 'preserve-caught-error': 'off', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-explicit-any': 'off', diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 7ad9f1a1b..e52826903 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1075,7 +1075,10 @@ export class DeviceWrapper { ); // Load the reference image buffer from disk - const referencePath = path.join('run', 'test', 'specs', 'media', referenceImageName); + const referencePath = path.join('run', 'test', 'media', referenceImageName); + await fs.access(referencePath).catch(() => { + throw new Error(`Reference image not found: ${referencePath}`); + }); const referenceBuffer = await fs.readFile(referencePath); let bestMatch: { @@ -1876,7 +1879,10 @@ export class DeviceWrapper { public async pushMediaToDevice( mediaFileName: 'profile_picture.jpg' | 'test_file.pdf' | 'test_image.jpg' | 'test_video.mp4' ) { - const filePath = path.join('run', 'test', 'specs', 'media', mediaFileName); + const filePath = path.join('run', 'test', 'media', mediaFileName); + await fs.access(filePath).catch(() => { + throw new Error(`Media file not found: ${filePath}`); + }); if (this.isIOS()) { // Push file to simulator await runScriptAndLog(`xcrun simctl addmedia ${this.udid} ${filePath}`, true); diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index 5073b2323..79a008534 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -42,7 +42,7 @@ const DEVICE_CONFIG = { runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-26-2', // xcrun simctl list runtimes }; -const MEDIA_ROOT = path.join('run', 'test', 'specs', 'media'); +const MEDIA_ROOT = path.join('run', 'test', 'media'); const MEDIA_FILES = { images: ['profile_picture.jpg', 'test_image.jpg'], videos: ['test_video.mp4'], From b736afb3bae8d6ad2527c570318094d77b03aef9 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 13:59:59 +1100 Subject: [PATCH 083/184] fix: run allure files from correct dir --- github/actions/generate-publish-test-report/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml index aca9fafa4..fccc2bf99 100644 --- a/github/actions/generate-publish-test-report/action.yml +++ b/github/actions/generate-publish-test-report/action.yml @@ -24,7 +24,7 @@ runs: - name: Generate Allure report shell: bash run: | - npx ts-node run/test/specs/utils/allure/closeRun.ts + npx ts-node run/test/utils/allure/closeRun.ts env: PLATFORM: ${{ inputs.PLATFORM }} BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }} @@ -40,7 +40,7 @@ runs: id: publish shell: bash run: | - npx ts-node run/test/specs/utils/allure/publishReport.ts + npx ts-node run/test/utils/allure/publishReport.ts env: PLATFORM: ${{ inputs.PLATFORM }} BUILD_NUMBER: ${{ inputs.BUILD_NUMBER }} From 3eb2c86fa42bb36f4f2aab7a52737f57cde15774 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 15:51:30 +1100 Subject: [PATCH 084/184] Merge remote-tracking branch 'origin/dev' into feat/pro --- run/test/{specs => }/media/animated_profile_picture.gif | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename run/test/{specs => }/media/animated_profile_picture.gif (100%) diff --git a/run/test/specs/media/animated_profile_picture.gif b/run/test/media/animated_profile_picture.gif similarity index 100% rename from run/test/specs/media/animated_profile_picture.gif rename to run/test/media/animated_profile_picture.gif From e347841ba22d762dd6b29147ceed5d4b1f86c920 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 13 Feb 2026 16:48:30 +1100 Subject: [PATCH 085/184] feat: expand char count tests to cover pro --- run/test/specs/message_length.spec.ts | 96 +++++++++++++++++++++------ run/test/utils/mock_pro.ts | 2 +- run/types/DeviceWrapper.ts | 21 +++++- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 7d720d9e3..81595d588 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -14,46 +14,97 @@ import { import { CTAButtonNegative } from '../locators/global'; import { PlusButton } from '../locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from '../locators/start_conversation'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; +import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { forceStopAndRestart } from '../utils/utilities'; -const maxChars = 2000; -const countdownThreshold = 1800; +const STANDARD_MAX_CHARS = 2000; +const PRO_MAX_CHARS = 10000; +const COUNTDOWN_START_THRESHOLD = 200; const messageLengthTestCases = [ { + pro: false, length: 1799, - char: 'a', shouldSend: true, description: 'no countdown shows, message sends', }, - { length: 1800, char: 'b', shouldSend: true, description: 'countdown shows 200, message sends' }, - { length: 2000, char: 'c', shouldSend: true, description: 'countdown shows 0, message sends' }, { + pro: false, + length: 1800, + shouldSend: true, + description: 'countdown shows 200, message sends', + }, + { + pro: false, + length: 2000, + shouldSend: true, + description: 'countdown shows 0, message sends', + }, + { + pro: false, length: 2001, - char: 'd', + shouldSend: false, + description: 'countdown shows -1, cannot send message', + }, + { + pro: true, + length: 9799, + shouldSend: true, + description: 'no countdown shows, message sends', + }, + { + pro: true, + length: 9800, + shouldSend: true, + description: 'countdown shows 200, message sends', + }, + { + pro: true, + length: 10000, + shouldSend: true, + description: 'countdown shows 0, message sends', + }, + { + pro: true, + length: 10001, shouldSend: false, description: 'countdown shows -1, cannot send message', }, ]; for (const testCase of messageLengthTestCases) { + const proSuffix = testCase.pro ? `Pro` : `non Pro`; bothPlatformsIt({ - title: `Message length limit (${testCase.length} chars)`, + title: `Message length limit (${testCase.length} chars ${proSuffix})`, risk: 'high', countOfDevicesNeeded: 1, allureSuites: { parent: 'Sending Messages', suite: 'Rules', }, - allureDescription: `Verifies message length behavior at ${testCase.length} characters - ${testCase.description}`, + allureDescription: `Verifies message length behavior at ${testCase.length} characters - ${testCase.description} (${proSuffix})`, testCb: async (platform: SupportedPlatformsType, testInfo: TestInfo) => { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); + if (testCase.pro) { + const paymentProvider = platform === 'ios' ? 'apple' : 'google'; + await makeAccountPro({ + mnemonic: alice.recoveryPhrase, + provider: paymentProvider, + }); + await forceStopAndRestart(device); + } + // Send message to self to bring up Note to Self conversation await test.step(TestSteps.OPEN.NTS, async () => { await device.clickOnElementAll(new PlusButton(device)); @@ -64,12 +115,15 @@ for (const testCase of messageLengthTestCases) { }); await test.step(`Type ${testCase.length} chars, check countdown`, async () => { + const expectedMax = testCase.pro ? PRO_MAX_CHARS : STANDARD_MAX_CHARS; const expectedCount = - testCase.length < countdownThreshold ? null : (maxChars - testCase.length).toString(); + testCase.length < expectedMax - COUNTDOWN_START_THRESHOLD + ? null + : (expectedMax - testCase.length).toString(); // Construct the string of desired length - const message = testCase.char.repeat(testCase.length); - await device.inputText(message, new MessageInput(device)); + const message = 'x'.repeat(testCase.length); + await device.inputText(message, new MessageInput(device), true); // Does the countdown appear? if (expectedCount) { @@ -85,21 +139,19 @@ for (const testCase of messageLengthTestCases) { // Is the message short enough to send? if (testCase.shouldSend) { await device.waitForTextElementToBePresent(new MessageBody(device, message)); - } else if (platform === 'ios') { - // iOS: Modal appears, verify and dismiss + } else if (!testCase.pro) { + // For Non Pro, a CTA appears + await device.checkCTA('longerMessages'); + await device.clickOnElementAll(new CTAButtonNegative(device)); + await device.verifyElementNotPresent(new MessageBody(device, message)); + } else if (testCase.pro) { + // For Pro, a normal message length dialog appears await device.checkModalStrings( tStripped('modalMessageTooLongTitle'), - tStripped('modalMessageTooLongDescription', { limit: maxChars.toString() }) + tStripped('modalMessageTooLongDescription', { limit: expectedMax.toString() }) ); await device.clickOnElementAll(new MessageLengthOkayButton(device)); await device.verifyElementNotPresent(new MessageBody(device, message)); - } else { - // Android: CTA appears, verify and dismiss - // Post-Pro is active on debug/qa builds by default - // This will be the default for both platforms once Pro is live - await device.checkCTA('longerMessages'); - await device.clickOnElementAll(new CTAButtonNegative(device)); - await device.verifyElementNotPresent(new MessageBody(device, message)); } }); diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index 32e8b1261..4a1d6833d 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -69,7 +69,7 @@ function getWordlist(): string[] { return WORDLIST_CACHE; } - const wordlistPath = join(__dirname, '../../../../english_wordlist.txt'); + const wordlistPath = join(__dirname, '../../../english_wordlist.txt'); const content = readFileSync(wordlistPath, 'utf-8'); const words = content .split('\n') diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f93cf3184..e11d0d0e9 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1833,7 +1833,8 @@ export class DeviceWrapper { public async inputText( textToInput: string, - args: LocatorsInterface | ({ maxWait?: number } & StrategyExtractionObj) + args: LocatorsInterface | ({ maxWait?: number } & StrategyExtractionObj), + paste: boolean = false ) { const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -1844,7 +1845,23 @@ export class DeviceWrapper { throw new Error(`inputText: Did not find element with locator: ${JSON.stringify(locator)}`); } - await this.setValueImmediate(textToInput, el.ELEMENT); + if (paste) { + await this.click(el.ELEMENT); + if (this.isAndroid()) { + await this.toAndroid().setClipboard( + Buffer.from(textToInput).toString('base64'), + 'plaintext' + ); + // KEYCODE_PASTE = 279 + await this.toAndroid().pressKeyCode(279); + } else { + await this.toIOS().mobileSetPasteboard(textToInput, 'utf8'); + // XCUIKeyModifierCommand = 1 << 4 = 16 + await this.toIOS().mobileKeys([{ key: 'v', modifierFlags: 16 }]); + } + } else { + await this.setValueImmediate(textToInput, el.ELEMENT); + } } public async getAttribute(attribute: string, elementId: string) { From 99ab2c11179881b815a25c42f54dfe18a2ea04b0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 16 Feb 2026 10:15:38 +1100 Subject: [PATCH 086/184] refactor: remove dead code --- appium_next.d.ts | 16 ---------------- run/localizer/englishStrippedStr.ts | 6 ------ run/types/tuple.d.ts | 9 --------- 3 files changed, 31 deletions(-) delete mode 100644 appium_next.d.ts delete mode 100644 run/localizer/englishStrippedStr.ts delete mode 100644 run/types/tuple.d.ts diff --git a/appium_next.d.ts b/appium_next.d.ts deleted file mode 100644 index bed033dc9..000000000 --- a/appium_next.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-object-type */ -import { ExternalDriver } from '@appium/types/build/lib/driver'; - -// typings comes from : -// node_modules/@appium/types/build/lib/driver.d.ts -// BUT. they are defined as optional, so here we just copy and paste the one we need, and hardcode the fact that they are defined. -// We need to do this, because the iosDriver and the androidDriver do not export the typings (where they defined that those function exists) - -export interface MightBeUndefinedDeviceType extends ExternalDriver {} - -export type AppiumNextDeviceType = { - pushFile(remotePath: string, payloadBase64: string): Promise; - - // not sure at all - touchMove(x: number, y: number): Promise; -}; diff --git a/run/localizer/englishStrippedStr.ts b/run/localizer/englishStrippedStr.ts deleted file mode 100644 index e90253ada..000000000 --- a/run/localizer/englishStrippedStr.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LocalizedStringBuilder, MergedLocalizerTokens } from "./lib"; - -export function englishStrippedStr(token: T) { - const builder = new LocalizedStringBuilder(token).stripIt().forceEnglish(); - return builder; -} diff --git a/run/types/tuple.d.ts b/run/types/tuple.d.ts deleted file mode 100644 index 8455b6874..000000000 --- a/run/types/tuple.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -type TupleOf> = R['length'] extends N - ? R - : TupleOf; - -export type Tuple = N extends N - ? number extends N - ? Array - : TupleOf - : never; From d783973bef1f324f59e414f6ad0a72ccfa4c9ea0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 16 Feb 2026 11:08:27 +1100 Subject: [PATCH 087/184] fix: more Android 1.32.0 fixes --- run/test/locators/settings.ts | 17 +++++++++ run/test/specs/slow_mode_background.spec.ts | 16 +++++++-- run/test/specs/voice_calls.spec.ts | 9 ++--- run/test/utils/mock_pro.ts | 39 ++++++++------------- run/types/DeviceWrapper.ts | 10 ++++++ run/types/testing.ts | 2 ++ 6 files changed, 62 insertions(+), 31 deletions(-) diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index 7761885bd..c95e60a85 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -137,6 +137,23 @@ export class DonationsMenuItem extends LocatorsInterface { } } +export class EnableVoiceCalls extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'preferences-dialog-option-enable', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Continue', + } as const; + } + } +} + export class HideRecoveryPasswordButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/specs/slow_mode_background.spec.ts b/run/test/specs/slow_mode_background.spec.ts index 62cff9a22..9d1a331f1 100644 --- a/run/test/specs/slow_mode_background.spec.ts +++ b/run/test/specs/slow_mode_background.spec.ts @@ -1,10 +1,11 @@ -import test, { TestInfo } from '@playwright/test'; +import { test, TestInfo } from '@playwright/test'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { androidIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { BackgroundPermsAllowButton } from '../locators/home'; +import { NotificationsMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; import { closeApp, @@ -48,7 +49,18 @@ async function slowModeBackgroundModal(platform: SupportedPlatformsType, testInf text: 'Allow', }); }); - // The test ends here since there is no good way to verify that the specific toggle is ON. + await test.step('Verify Background usage toggle is turned ON', async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new NotificationsMenuItem(device)); + await device.assertAttribute( + { + strategy: 'id', + selector: 'preferences-option-whitelist-toggle', + }, + 'checked', + 'true' + ); + }); } finally { // App must be uninstalled to prevent state pollution (background permission is tied to app install) await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 49925f26a..d2807810e 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -5,6 +5,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { CloseSettings } from '../locators'; import { CallButton, NotificationsModalButton, NotificationSwitch } from '../locators/conversation'; +import { EnableVoiceCalls } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { sleepFor } from '../utils/index'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; @@ -58,7 +59,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoModalDescription') ); }); - await alice1.clickOnByAccessibilityID('Continue'); + await alice1.clickOnElementAll(new EnableVoiceCalls(alice1)); // Need to allow microphone access await alice1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -103,7 +104,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await bob1.clickOnByAccessibilityID('Continue'); + await bob1.clickOnElementAll(new EnableVoiceCalls(bob1)); // Need to allow microphone access await bob1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -178,7 +179,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await alice1.clickOnByAccessibilityID('Enable'); + await alice1.clickOnElementAll(new EnableVoiceCalls(alice1)); }); await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' @@ -231,7 +232,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'accessibility id', selector: 'Settings', }); - await bob1.clickOnByAccessibilityID('Enable'); + await bob1.clickOnElementAll(new EnableVoiceCalls(bob1)); await bob1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index 4a1d6833d..9a612a419 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -26,15 +26,15 @@ import { join } from 'path'; import { PRO_BACKEND_URL } from '../../constants'; -export type PaymentProvider = 'apple' | 'google'; +type PaymentProvider = 'apple' | 'google'; -export interface MakeAccountProParams { +type MakeAccountProParams = { mnemonic: string; provider: PaymentProvider; dryRun?: boolean; // If true, build and print the request but don't send it -} +}; -export interface AddProPaymentRequest { +type AddProPaymentRequest = { version: number; master_pkey: string; rotating_pkey: string; @@ -46,21 +46,21 @@ export interface AddProPaymentRequest { google_order_id?: string; apple_tx_id?: string; }; -} +}; -export interface ProProof { +type ProProof = { version: number; expiry_unix_ts_ms: number; gen_index_hash: string; rotating_pkey: string; sig: string; -} +}; -export interface AddProPaymentResponse { +type AddProPaymentResponse = { status: number; result?: ProProof; errors?: string[]; -} +}; let WORDLIST_CACHE: string[] | null = null; @@ -85,7 +85,7 @@ function getWordlist(): string[] { } // Decodes a 13-word recovery phrase a 16-byte seed hex string. */ -export function mnemonicToSeedHex(mnemonic: string): string { +function mnemonicToSeedHex(mnemonic: string): string { const wordlist = getWordlist(); const n = wordlist.length; // 1626 @@ -153,19 +153,8 @@ function padSeed(seedHex: string): Uint8Array { return padded; } -// Derives the account-level Ed25519 keypair by zero-padding the 16-byte seed to 32 bytes. -export function deriveAccountEd25519Keypair(seedHex: string): { - privateKey: Uint8Array; - publicKey: Uint8Array; -} { - const padded = padSeed(seedHex); - const privateKey = padded; - const publicKey = ed25519.getPublicKey(privateKey); - return { privateKey, publicKey }; -} - // Derives the Pro master keypair from the seed using Blake2b with "SessionProRandom" as the key. -export function deriveProMasterKey(seedHex: string): { +function deriveProMasterKey(seedHex: string): { privateKey: Uint8Array; publicKey: Uint8Array; } { @@ -184,7 +173,7 @@ export function deriveProMasterKey(seedHex: string): { } // Generates a random ephemeral rotating keypair for the payment request. -export function generateRotatingKey(): { privateKey: Uint8Array; publicKey: Uint8Array } { +function generateRotatingKey(): { privateKey: Uint8Array; publicKey: Uint8Array } { const privateKey = ed25519.utils.randomSecretKey(); const publicKey = ed25519.getPublicKey(privateKey); return { privateKey, publicKey }; @@ -236,7 +225,7 @@ function makeAddProPaymentHash( } // Builds a signed add_pro_payment request body with fake payment tokens. -export function buildAddProPaymentRequest( +function buildAddProPaymentRequest( masterKey: { privateKey: Uint8Array; publicKey: Uint8Array }, rotatingKey: { privateKey: Uint8Array; publicKey: Uint8Array }, provider: PaymentProvider @@ -293,7 +282,7 @@ export function buildAddProPaymentRequest( } // POSTs the payment request to the Pro backend with retries and timeout. -export async function addProPayment( +async function addProPayment( backendUrl: string, request: AddProPaymentRequest, { maxAttempts = 3, timeout = 10_000 } = {} diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index e11d0d0e9..3847d7189 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1868,6 +1868,16 @@ export class DeviceWrapper { return this.toShared().getAttribute(attribute, elementId); } + public async assertAttribute( + element: LocatorsInterface | StrategyExtractionObj, + attribute: string, + value: string + ) { + const el = await this.waitForTextElementToBePresent(element); + const received = await this.getAttribute(attribute, el.ELEMENT); + expect(received, 'Element attribute value mismatch').toBe(value); + } + public async disappearRadioButtonSelected( platform: SupportedPlatformsType, timeOption: DISAPPEARING_TIMES diff --git a/run/types/testing.ts b/run/types/testing.ts index f547a2efa..2aec099f1 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -565,8 +565,10 @@ export type Id = | 'open-survey-button' | 'Open' | 'Open URL' + | 'preferences-dialog-option-enable' | 'preferences-option-blocked-contacts' | 'preferences-option-read-receipt' + | 'preferences-option-whitelist-toggle' | 'preferred-display-name' | 'Privacy' | 'Privacy policy button' From d379105c74483b57a0421bab6770c7be9a97c3d2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 16 Feb 2026 12:32:45 +1100 Subject: [PATCH 088/184] feat: offset long click gesture --- run/test/specs/group_message_voice.spec.ts | 4 +- run/test/specs/message_voice.spec.ts | 4 +- run/types/DeviceWrapper.ts | 54 +++++++++++++++++----- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index 028cd5ffd..3cd346c33 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -40,7 +40,9 @@ async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: device.waitForTextElementToBePresent(new VoiceMessage(device)) ) ); - await bob1.longPressMessage(new VoiceMessage(bob1)); + // The voice message long tap must be offset so that it doesn't tap the scrubber + // As this starts playback and does not open the long press menu + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index e5802e79d..68164a123 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -29,7 +29,9 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test await alice1.waitForTextElementToBePresent(new VoiceMessage(alice1)); await bob1.trustAttachments(alice.userName); await sleepFor(500); - await bob1.longPressMessage(new VoiceMessage(bob1)); + // The voice message long tap must be offset so that it doesn't tap the scrubber + // As this starts playback and does not open the long press menu + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 3847d7189..11b58adc3 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -620,18 +620,47 @@ export class DeviceWrapper { } } - public async longClick(element: AppiumNextElementType, durationMs: number) { + // Appium taps elements in their center but sometimes that is not desirable + // The native methods apply the tap offset from the top left corner + // For a more intuitive offset calculation, this method allows us to + // define offsets based on the element center + private async calculateGestureOffset( + element: AppiumNextElementType, + offset: Coordinates + ): Promise { + const rect = await this.getElementRect(element.ELEMENT); + if (!rect) { + throw new Error('Failed to resolve element rect for offset calculation'); + } + const { width, height } = rect; + const centerX = Math.round(width / 2); + const centerY = Math.round(height / 2); + // Clamp offset to element bounds + const x = Math.min(Math.max(centerX + offset.x, 0), rect.width); + const y = Math.min(Math.max(centerY + offset.y, 0), rect.height); + return { x, y }; + } + + /** + * @param offset Pixel offset from the element center. + * If an offset is necessary, both x and y must be defined, otherwise Appium doesn't apply the offset parameter. + */ + public async longClick(element: AppiumNextElementType, durationMs: number, offset?: Coordinates) { + let xOffset: number | undefined; + let yOffset: number | undefined; + + if (offset) { + const offsetCoordinates = await this.calculateGestureOffset(element, offset); + xOffset = offsetCoordinates.x; + yOffset = offsetCoordinates.y; + } + if (this.isIOS()) { // iOS takes a number in seconds const duration = Math.floor(durationMs / 1000); - return this.toIOS().mobileTouchAndHold(duration, undefined, undefined, element.ELEMENT); + return this.toIOS().mobileTouchAndHold(duration, xOffset, yOffset, element.ELEMENT); } - return this.toAndroid().mobileLongClickGesture( - element.ELEMENT, - undefined, - undefined, - durationMs - ); + return this.toAndroid().mobileLongClickGesture(element.ELEMENT, xOffset, yOffset, durationMs); } public async clickOnByAccessibilityID( @@ -745,7 +774,8 @@ export class DeviceWrapper { * @throws if message not found or context menu fails to appear within maxWait */ public async longPressMessage( - args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) + args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), + options?: { offset?: Coordinates } ): Promise { const { text, maxWait = 10_000 } = args; const locator = args instanceof LocatorsInterface ? args.build() : args; @@ -768,9 +798,11 @@ export class DeviceWrapper { if (!el) { return { success: false, error: `Message not found: ${displayText}` }; } - + if (options?.offset) { + console.log(`Offsetting long press by x=${options?.offset?.x}, y=${options?.offset?.y}`); + } // Attempt long click - await this.longClick(el, 2000); + await this.longClick(el, 2000, options?.offset); // Check if context menu appeared const longPressSuccess = await this.waitForTextElementToBePresent({ From 48d290766dc7e68e9db5da32abce4c3391b7f481 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 16 Feb 2026 14:30:31 +1100 Subject: [PATCH 089/184] fix: remove duplicate definition --- run/types/DeviceWrapper.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 11b58adc3..28f7b7360 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -80,6 +80,7 @@ import { SupportedPlatformsType } from '../test/utils/open_app'; import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; import { AccessibilityId, + Coordinates, DISAPPEARING_TIMES, Group, Id, @@ -90,10 +91,6 @@ import { XPath, } from './testing'; -export type Coordinates = { - x: number; - y: number; -}; export type ActionSequence = { actions: string; }; @@ -799,7 +796,7 @@ export class DeviceWrapper { return { success: false, error: `Message not found: ${displayText}` }; } if (options?.offset) { - console.log(`Offsetting long press by x=${options?.offset?.x}, y=${options?.offset?.y}`); + this.log(`Offsetting long press by x=${options?.offset?.x}, y=${options?.offset?.y}`); } // Attempt long click await this.longClick(el, 2000, options?.offset); From b810e872bd0ebf45499d412ddcd3c8336fe10c01 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 09:40:18 +1100 Subject: [PATCH 090/184] fix: dismiss CTA if present --- run/test/specs/message_length.spec.ts | 2 ++ run/test/utils/utilities.ts | 3 +++ run/types/DeviceWrapper.ts | 14 +++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 81595d588..21782420f 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -102,7 +102,9 @@ for (const testCase of messageLengthTestCases) { mnemonic: alice.recoveryPhrase, provider: paymentProvider, }); + // Restart to notify app of Pro status change await forceStopAndRestart(device); + await device.dismissCTA(); } // Send message to self to bring up Note to Self conversation diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index e0461f1d7..c21434f91 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -5,6 +5,7 @@ import path from 'path'; import * as util from 'util'; import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { PlusButton } from '../locators/home'; import { androidAppActivity, androidAppPackage } from './capabilities_android'; import { iOSBundleId } from './capabilities_ios'; import { sleepFor } from './sleep_for'; @@ -146,4 +147,6 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise await runScriptAndLog(`xcrun simctl launch ${device.udid} ${iOSBundleId}`, true); await sleepFor(1_000); } + // Ensure we're on the home screen again + await device.waitForTextElementToBePresent(new PlusButton(device)); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 28f7b7360..6b1e579f0 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -639,7 +639,7 @@ export class DeviceWrapper { } /** - * @param offset Pixel offset from the element center. + * @param offset Pixel offset from the element center. * If an offset is necessary, both x and y must be defined, otherwise Appium doesn't apply the offset parameter. */ public async longClick(element: AppiumNextElementType, durationMs: number, offset?: Coordinates) { @@ -2658,6 +2658,18 @@ export class DeviceWrapper { ]); } + // Dismiss any CTA if it shows + public async dismissCTA(): Promise { + const hasCTAAppeared = await this.doesElementExist({ + ...new CTAButtonNegative(this).build(), + maxWait: 8_000, + }); + if (hasCTAAppeared) { + this.log('Dismissing CTA'); + await this.clickOnElementAll(new CTAButtonNegative(this)); + } + } + public async getElementPixelColor(args: LocatorsInterface): Promise { // Wait for the element to be present const element = await this.waitForTextElementToBePresent(args); From bcf6e746f2143060cca6b07500199d41d30a3ddd Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 09:42:28 +1100 Subject: [PATCH 091/184] fix: scope secret to ios workflow --- .github/workflows/ios-regression.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index a17a462a4..2814afcb2 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -90,6 +90,7 @@ jobs: AVD_MANAGER_FULL_PATH: '' ANDROID_SYSTEM_IMAGE: '' EMULATOR_FULL_PATH: '' + SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} steps: - uses: actions/checkout@v6 From fac2aefab65035db6fd3ed612efc0fc14939c4a2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 09:42:44 +1100 Subject: [PATCH 092/184] chore: fix submodule command --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a57339500..8232b6be7 100644 --- a/README.md +++ b/README.md @@ -77,14 +77,13 @@ nvm install nvm use git lfs install git lfs pull -git submodule update --init --recursive --remote +git submodule update --init --recursive pnpm install --frozen-lockfile ``` Then, choose an option: ``` -pnpm tsc # Build typescript files pnpm run test # Run all the tests Platform specific @@ -92,5 +91,5 @@ pnpm run test-android # To run just Android tests pnpm run test-ios # To run just iOS tests pnpm run test-one 'Name of test' # To run one test (on both platforms) -pnpm run test-one 'Name of test android/ios' # To run one test on either platform +pnpm run test-one 'Name of test @android/@ios' # To run one test on either platform ``` From be06fa3f8b534262473e84ac61d120ba7d2cbc6a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 09:42:49 +1100 Subject: [PATCH 093/184] chore: linting --- run/test/specs/app_disguise_set.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/run/test/specs/app_disguise_set.spec.ts b/run/test/specs/app_disguise_set.spec.ts index f9a32af50..c0abcbf5a 100644 --- a/run/test/specs/app_disguise_set.spec.ts +++ b/run/test/specs/app_disguise_set.spec.ts @@ -14,7 +14,12 @@ import { } from '../locators/settings'; import { sleepFor } from '../utils'; import { newUser } from '../utils/create_account'; -import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType, uninstallApp } from '../utils/open_app'; +import { + closeApp, + openAppOnPlatformSingleDevice, + SupportedPlatformsType, + uninstallApp, +} from '../utils/open_app'; bothPlatformsItSeparate({ title: 'App disguise set icon', From 6d858b6e68de69ec7f7a02b2a8b3b8bcabc57907 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 10:05:38 +1100 Subject: [PATCH 094/184] fix: change locators in calls tests --- run/test/locators/settings.ts | 36 ++++++++++++------------ run/test/specs/disappearing_call.spec.ts | 7 +++-- run/test/specs/voice_calls.spec.ts | 16 +++++------ 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index c95e60a85..a1f625077 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -137,23 +137,6 @@ export class DonationsMenuItem extends LocatorsInterface { } } -export class EnableVoiceCalls extends LocatorsInterface { - public build() { - switch (this.platform) { - case 'android': - return { - strategy: 'id', - selector: 'preferences-dialog-option-enable', - } as const; - case 'ios': - return { - strategy: 'accessibility id', - selector: 'Continue', - } as const; - } - } -} - export class HideRecoveryPasswordButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -170,6 +153,7 @@ export class HideRecoveryPasswordButton extends LocatorsInterface { } } } + export class NotificationsMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -187,7 +171,6 @@ export class NotificationsMenuItem extends LocatorsInterface { } } } - export class PathMenuItem extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -288,6 +271,7 @@ export class SaveNameChangeButton extends LocatorsInterface { } } } + export class SaveProfilePictureButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -321,6 +305,22 @@ export class SelectAppIcon extends LocatorsInterface { } } } +export class SettingsModalsEnableButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'preferences-dialog-option-enable', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Continue', + } as const; + } + } +} export class UserAvatar extends LocatorsInterface { public build() { diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index e7c16ac42..ceba5b8d0 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -5,7 +5,8 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { CloseSettings } from '../locators'; -import { CallButton, NotificationsModalButton, NotificationSwitch } from '../locators/conversation'; +import { CallButton, NotificationSwitch } from '../locators/conversation'; +import { SettingsModalsEnableButton } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { sleepFor } from '../utils'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; @@ -133,11 +134,11 @@ async function disappearingCallMessage1o1Android( strategy: 'accessibility id', selector: 'Settings', }); - await alice1.clickOnByAccessibilityID('Enable'); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - await alice1.clickOnElementAll(new NotificationsModalButton(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementAll(new NotificationSwitch(alice1)); // Return to conversation await alice1.navigateBack(false); diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index d2807810e..c71789ccf 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -4,8 +4,8 @@ import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsItSeparate } from '../../types/sessionIt'; import { CloseSettings } from '../locators'; -import { CallButton, NotificationsModalButton, NotificationSwitch } from '../locators/conversation'; -import { EnableVoiceCalls } from '../locators/settings'; +import { CallButton, NotificationSwitch } from '../locators/conversation'; +import { SettingsModalsEnableButton } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { sleepFor } from '../utils/index'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; @@ -59,7 +59,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoModalDescription') ); }); - await alice1.clickOnElementAll(new EnableVoiceCalls(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); // Need to allow microphone access await alice1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -104,7 +104,7 @@ async function voiceCallIos(platform: SupportedPlatformsType, testInfo: TestInfo tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await bob1.clickOnElementAll(new EnableVoiceCalls(bob1)); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); // Need to allow microphone access await bob1.modalPopup({ strategy: 'accessibility id', selector: 'Allow' }); await sleepFor(1_000); @@ -179,7 +179,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test tStripped('callsVoiceAndVideoBeta'), tStripped('callsVoiceAndVideoModalDescription') ); - await alice1.clickOnElementAll(new EnableVoiceCalls(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); }); await alice1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' @@ -189,7 +189,7 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test tStripped('sessionNotifications'), tStripped('callsNotificationsRequired') ); - await alice1.clickOnElementAll(new NotificationsModalButton(alice1)); + await alice1.clickOnElementAll(new SettingsModalsEnableButton(alice1)); await alice1.clickOnElementAll(new NotificationSwitch(alice1)); }); await alice1.navigateBack(false); @@ -232,11 +232,11 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'accessibility id', selector: 'Settings', }); - await bob1.clickOnElementAll(new EnableVoiceCalls(bob1)); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); await bob1.clickOnElementById( 'com.android.permissioncontroller:id/permission_allow_foreground_only_button' ); - await bob1.clickOnElementAll(new NotificationsModalButton(bob1)); + await bob1.clickOnElementAll(new SettingsModalsEnableButton(bob1)); await bob1.clickOnElementAll(new NotificationSwitch(bob1)); await bob1.navigateBack(false); await bob1.navigateBack(false); From 17822d1116174392a2b341796d7e19e51e1bea70 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 10:55:49 +1100 Subject: [PATCH 095/184] feat: add share in session test --- .../user_actions_share_to_session.spec.ts | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) 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 952c18384..75cbcfef4 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -2,10 +2,16 @@ import { test, type TestInfo } from '@playwright/test'; import { testImage } from '../../constants/testfiles'; import { TestSteps } from '../../types/allure'; -import { bothPlatformsIt } from '../../types/sessionIt'; +import { androidIt, bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ImageName, ShareExtensionIcon } from '../locators'; -import { MessageBody, MessageInput, SendButton } from '../locators/conversation'; +import { + ConversationHeaderName, + MediaMessage, + MessageBody, + MessageInput, + SendButton, +} from '../locators/conversation'; import { PhotoLibrary } from '../locators/external'; import { Contact } from '../locators/global'; import { open_Alice1_Bob1_friends } from '../state_builder'; @@ -14,7 +20,7 @@ import { handlePhotosFirstTimeOpen } from '../utils/handle_first_open'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; bothPlatformsIt({ - title: 'Share to session', + title: 'Share to Session', risk: 'medium', testCb: shareToSession, countOfDevicesNeeded: 2, @@ -25,6 +31,19 @@ bothPlatformsIt({ allureDescription: `Verifies that a user can share an image from the photo gallery to Session`, }); +// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. +androidIt({ + title: 'Share within Session', + risk: 'medium', + testCb: shareInSession, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'User Actions', + suite: 'Share to Session', + }, + allureDescription: `Verifies that a user can share an image from one Session conversation to another (forwarding)`, +}); + async function shareToSession(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, @@ -76,3 +95,35 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn await closeApp(alice1, bob1); }); } + +async function shareInSession(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { + devices: { alice1, bob1 }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: true, + testInfo, + }); + }); + const testMessage = 'Testing forwarding an image within Session'; + await test.step(TestSteps.SEND.IMAGE, async () => { + await alice1.sendImage(testMessage); + }); + await test.step('Share image to another Session conversation', async () => { + await alice1.clickOnElementAll(new MediaMessage(alice1)); + await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Share' }); + await alice1.clickOnElementAll(new Contact(alice1, 'Note to Self')); + await alice1.inputText(testMessage, new MessageInput(alice1)); + await alice1.clickOnElementAll(new SendButton(alice1)); + await alice1.waitForLoadingOnboarding(); + }); + await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { + await alice1.waitForTextElementToBePresent(new ConversationHeaderName(alice1, 'Note to Self')); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, testMessage)); + await alice1.matchAndTapImage(new MediaMessage(alice1).build(), testImage); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} From 34292c25d1b589db20d80af1c2a2438b7b618d40 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Feb 2026 10:55:49 +1100 Subject: [PATCH 096/184] feat: add share in session test --- .../user_actions_share_to_session.spec.ts | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) 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 952c18384..75cbcfef4 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -2,10 +2,16 @@ import { test, type TestInfo } from '@playwright/test'; import { testImage } from '../../constants/testfiles'; import { TestSteps } from '../../types/allure'; -import { bothPlatformsIt } from '../../types/sessionIt'; +import { androidIt, bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { ImageName, ShareExtensionIcon } from '../locators'; -import { MessageBody, MessageInput, SendButton } from '../locators/conversation'; +import { + ConversationHeaderName, + MediaMessage, + MessageBody, + MessageInput, + SendButton, +} from '../locators/conversation'; import { PhotoLibrary } from '../locators/external'; import { Contact } from '../locators/global'; import { open_Alice1_Bob1_friends } from '../state_builder'; @@ -14,7 +20,7 @@ import { handlePhotosFirstTimeOpen } from '../utils/handle_first_open'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; bothPlatformsIt({ - title: 'Share to session', + title: 'Share to Session', risk: 'medium', testCb: shareToSession, countOfDevicesNeeded: 2, @@ -25,6 +31,19 @@ bothPlatformsIt({ allureDescription: `Verifies that a user can share an image from the photo gallery to Session`, }); +// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. +androidIt({ + title: 'Share within Session', + risk: 'medium', + testCb: shareInSession, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'User Actions', + suite: 'Share to Session', + }, + allureDescription: `Verifies that a user can share an image from one Session conversation to another (forwarding)`, +}); + async function shareToSession(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, @@ -76,3 +95,35 @@ async function shareToSession(platform: SupportedPlatformsType, testInfo: TestIn await closeApp(alice1, bob1); }); } + +async function shareInSession(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { + devices: { alice1, bob1 }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: true, + testInfo, + }); + }); + const testMessage = 'Testing forwarding an image within Session'; + await test.step(TestSteps.SEND.IMAGE, async () => { + await alice1.sendImage(testMessage); + }); + await test.step('Share image to another Session conversation', async () => { + await alice1.clickOnElementAll(new MediaMessage(alice1)); + await alice1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Share' }); + await alice1.clickOnElementAll(new Contact(alice1, 'Note to Self')); + await alice1.inputText(testMessage, new MessageInput(alice1)); + await alice1.clickOnElementAll(new SendButton(alice1)); + await alice1.waitForLoadingOnboarding(); + }); + await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { + await alice1.waitForTextElementToBePresent(new ConversationHeaderName(alice1, 'Note to Self')); + await alice1.waitForTextElementToBePresent(new MessageBody(alice1, testMessage)); + await alice1.matchAndTapImage(new MediaMessage(alice1).build(), testImage); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} From 5b35ddadc3b6ff0ea49da43fa5d4d935804dd150 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 09:09:44 +1100 Subject: [PATCH 097/184] chore: update screenshots --- run/screenshots/android/cta_donate.png | 4 ++-- run/screenshots/android/landingpage_new_account.png | 4 ++-- run/screenshots/android/settings_conversations.png | 4 ++-- run/screenshots/android/settings_notifications.png | 4 ++-- run/screenshots/android/settings_privacy.png | 4 ++-- run/test/specs/user_actions_share_to_session.spec.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/run/screenshots/android/cta_donate.png b/run/screenshots/android/cta_donate.png index 4902856eb..704211b7c 100644 --- a/run/screenshots/android/cta_donate.png +++ b/run/screenshots/android/cta_donate.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b5561d790a9d73b1afbf9c61262ce89b5e1cba85cecb8f445cd9634876943e4 -size 1146466 +oid sha256:3975c78efa3c6c09a91fc78f8d82db5d4d48a2fe1c2f11c681f11bb4c03eef07 +size 1115997 diff --git a/run/screenshots/android/landingpage_new_account.png b/run/screenshots/android/landingpage_new_account.png index 276e5373b..9a40983b4 100644 --- a/run/screenshots/android/landingpage_new_account.png +++ b/run/screenshots/android/landingpage_new_account.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cceb03631b9144e157f1c068f5917f58042c567adea70f3a9d74a9a478d9f02 -size 136014 +oid sha256:f0dcb7982a90edb0ea6b5de654429c670073dff99be415d9499d33bcacfc6868 +size 101390 diff --git a/run/screenshots/android/settings_conversations.png b/run/screenshots/android/settings_conversations.png index 983c94eab..45534e20b 100644 --- a/run/screenshots/android/settings_conversations.png +++ b/run/screenshots/android/settings_conversations.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:183f7aac856c61eb5f46638fc25dbb1755215712752b50f721698f3301a232d8 -size 156289 +oid sha256:c92c63b479821ffda45eba99884e4bd2cfa6fbe77e8d188508c43d9de35dfbfa +size 151360 diff --git a/run/screenshots/android/settings_notifications.png b/run/screenshots/android/settings_notifications.png index 0d9ee91e3..6229c00a9 100644 --- a/run/screenshots/android/settings_notifications.png +++ b/run/screenshots/android/settings_notifications.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e26cf860dbfb83a797c7465c826098601d275d15a4c69ee2dfe083d790661bfb -size 154470 +oid sha256:aec60ca8cc003a4d5e906a0b4c6a28e2b31c64ebfb5a601c998d5c22d3727c18 +size 147698 diff --git a/run/screenshots/android/settings_privacy.png b/run/screenshots/android/settings_privacy.png index 90b58f6b9..7da23a6d8 100644 --- a/run/screenshots/android/settings_privacy.png +++ b/run/screenshots/android/settings_privacy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e37c1174f4651cf2adb148517fe86ae36a6006bc50b71c9f2a1a329ac38f2940 -size 197778 +oid sha256:a464b633f442efc4a4515e9e6b52222de79b5a1ecc08c37966ab1df8eff1017d +size 209443 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 75cbcfef4..e5d6af1e5 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -31,7 +31,7 @@ bothPlatformsIt({ allureDescription: `Verifies that a user can share an image from the photo gallery to Session`, }); -// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. +// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. androidIt({ title: 'Share within Session', risk: 'medium', @@ -121,7 +121,7 @@ async function shareInSession(platform: SupportedPlatformsType, testInfo: TestIn await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await alice1.waitForTextElementToBePresent(new ConversationHeaderName(alice1, 'Note to Self')); await alice1.waitForTextElementToBePresent(new MessageBody(alice1, testMessage)); - await alice1.matchAndTapImage(new MediaMessage(alice1).build(), testImage); + await alice1.waitForTextElementToBePresent(new MediaMessage(alice1)); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1); From 346fbd2a3201d225cdd97c550428deb3b768ba0b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 10:59:37 +1100 Subject: [PATCH 098/184] fix: adjust pro tests for ios --- run/test/locators/global.ts | 28 +++--- run/test/locators/index.ts | 10 ++- run/test/specs/cta_donate_review.spec.ts | 6 +- run/test/specs/cta_donate_time.spec.ts | 2 +- ...r_actions_animated_profile_picture.spec.ts | 11 ++- run/test/utils/mock_pro.ts | 20 ++--- run/types/DeviceWrapper.ts | 88 +++++++++++++------ run/types/testing.ts | 7 ++ scripts/create_ios_simulators.ts | 2 +- 9 files changed, 116 insertions(+), 58 deletions(-) diff --git a/run/test/locators/global.ts b/run/test/locators/global.ts index 87c991c6e..4edae485f 100644 --- a/run/test/locators/global.ts +++ b/run/test/locators/global.ts @@ -100,7 +100,7 @@ export class CopyURLButton extends LocatorsInterface { } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) // See SES-4930 export class CTABody extends LocatorsInterface { public build() { @@ -112,14 +112,14 @@ export class CTABody extends LocatorsInterface { } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, + strategy: 'accessibility id', + selector: 'cta-body', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) // See SES-4930 export class CTAButtonNegative extends LocatorsInterface { public build() { @@ -133,13 +133,13 @@ export class CTAButtonNegative extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Maybe Later', + selector: 'cta-button-negative', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) // See SES-4930 export class CTAButtonPositive extends LocatorsInterface { public build() { @@ -153,13 +153,13 @@ export class CTAButtonPositive extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Donate', + selector: 'cta-button-positive', } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is not available +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA doesn't have features // See SES-4930 export class CTAFeature extends LocatorsInterface { private index: number; @@ -177,12 +177,16 @@ export class CTAFeature extends LocatorsInterface { selector: `new UiSelector().resourceId("cta-feature-${this.index}").childSelector(new UiSelector().className("android.widget.TextView"))`, } as const; case 'ios': - throw new Error('CTAFeature locator is not available on iOS'); + // iOS feature indexing starts at 1, Android at 0 + return { + strategy: 'accessibility id', + selector: `cta-feature-${this.index + 1}`, + } as const; } } } -// NOTE: This is meant to be a generic locator for all CTAs but for the time being the iOS implementation is limited to the Donate CTA +// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) // See SES-4930 export class CTAHeading extends LocatorsInterface { public build() { @@ -194,8 +198,8 @@ export class CTAHeading extends LocatorsInterface { } as const; case 'ios': return { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, + strategy: 'accessibility id', + selector: 'cta-heading', } as const; } } diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index f59f39793..603d02f33 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -550,6 +550,14 @@ export function describeLocator(locator: StrategyExtractionObj & { text?: string ? `${selector.substring(0, halfLength)}…${selector.substring(selector.length - halfLength)}` : selector; + // Trim text if too long, show beginning and end + const maxTextLength = 100; + const trimmedText = text + ? text.length > maxTextLength + ? `${text.substring(0, maxTextLength / 2)}…${text.substring(text.length - maxTextLength / 2)}` + : text + : undefined; + const base = `${strategy} "${trimmedSelector}"`; - return text ? `${base} and text "${text}"` : base; + return trimmedText ? `${base} and text "${trimmedText}"` : base; } diff --git a/run/test/specs/cta_donate_review.spec.ts b/run/test/specs/cta_donate_review.spec.ts index f34ba55ae..b99ccc816 100644 --- a/run/test/specs/cta_donate_review.spec.ts +++ b/run/test/specs/cta_donate_review.spec.ts @@ -51,7 +51,11 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await verifyPageScreenshot(device, platform, 'cta_donate', testInfo); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Open URL'), async () => { - await device.clickOnElementAll(new CTAButtonPositive(device)); + const positiveButton = await device.findWithFallback(new CTAButtonPositive(device), { + strategy: 'accessibility id', + selector: 'Donate', + } as const); + await device.click(positiveButton.ELEMENT); await device.checkModalStrings( tStripped('urlOpen'), tStripped('urlOpenDescription', { url: donateURL }) diff --git a/run/test/specs/cta_donate_time.spec.ts b/run/test/specs/cta_donate_time.spec.ts index c7e1f46c2..ee5308ba6 100644 --- a/run/test/specs/cta_donate_time.spec.ts +++ b/run/test/specs/cta_donate_time.spec.ts @@ -67,7 +67,7 @@ async function donateCTADoesntShowSixDaysAgo(platform: SupportedPlatformsType, t await test.step('Verify Donate CTA does not show', async () => { await Promise.all([ device.waitForTextElementToBePresent(new PlusButton(device)), - device.verifyNoCTAShows(), + device.verifyNoCTAShows('donate'), ]); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 6f90f770c..286d7bd1d 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -4,6 +4,7 @@ import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { PathMenuItem, UserAvatar } from '../locators/settings'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; @@ -32,8 +33,11 @@ bothPlatformsIt({ }); async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); await newUser(device, USERNAME.ALICE, { saveUserData: false }); return { device }; }); @@ -47,8 +51,11 @@ async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: Test }); } async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index 9a612a419..b9b3f6a3e 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -327,20 +327,10 @@ async function addProPayment( // Registers a test account as a Pro subscriber against the dev backend. export async function makeAccountPro(params: MakeAccountProParams): Promise { const { mnemonic, provider, dryRun = false } = params; - - console.log('Deriving keys from mnemonic...'); const seedHex = mnemonicToSeedHex(mnemonic); - const masterKey = deriveProMasterKey(seedHex); - - console.log(` Master pubkey: ${Buffer.from(masterKey.publicKey).toString('hex')}`); - - // Generate rotating key const rotatingKey = generateRotatingKey(); - console.log(` Rotating pubkey: ${Buffer.from(rotatingKey.publicKey).toString('hex')}`); - // Build request - console.log(`\nBuilding add_pro_payment request (${provider})...`); const request = buildAddProPaymentRequest(masterKey, rotatingKey, provider); console.log('\nRequest body:'); console.log(JSON.stringify(request, null, 2)); @@ -368,9 +358,13 @@ if (require.main === module) { const args = process.argv.slice(2); if (args.length < 2) { - console.error('Usage: ts-node mock_pro.ts [--dry-run]'); - console.error('Example: ts-node mock_pro.ts "word1 word2 ..." google'); - console.error(' ts-node mock_pro.ts "word1 word2 ..." apple --dry-run'); + console.error( + 'Usage: npx ts-node run/test/utils/mock_pro.ts [--dry-run]' + ); + console.error('Example: npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." google'); + console.error( + ' npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." apple --dry-run' + ); process.exit(1); } diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 6b1e579f0..48df6676a 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1866,27 +1866,24 @@ export class DeviceWrapper { paste: boolean = false ) { const locator = args instanceof LocatorsInterface ? args.build() : args; - - this.log('Locator being used:', locator); - const el = await this.waitForTextElementToBePresent({ ...locator }); - if (!el) { - throw new Error(`inputText: Did not find element with locator: ${JSON.stringify(locator)}`); - } if (paste) { - await this.click(el.ELEMENT); + // Set clipboard, press key-code for instant paste + await this.clickOnElementAll({ ...locator }); if (this.isAndroid()) { await this.toAndroid().setClipboard( Buffer.from(textToInput).toString('base64'), 'plaintext' ); - // KEYCODE_PASTE = 279 await this.toAndroid().pressKeyCode(279); } else { - await this.toIOS().mobileSetPasteboard(textToInput, 'utf8'); - // XCUIKeyModifierCommand = 1 << 4 = 16 - await this.toIOS().mobileKeys([{ key: 'v', modifierFlags: 16 }]); + // Use native paste UI, accept perms if needed + await this.toIOS().mobileSetPasteboard(textToInput); + await this.toIOS().mobileGetPasteboard(); + await this.processPermissions({ strategy: 'accessibility id', selector: 'Allow Paste' }); + await this.clickOnElementAll({ ...locator }); + await this.clickOnByAccessibilityID('Paste'); } } else { await this.setValueImmediate(textToInput, el.ELEMENT); @@ -2452,8 +2449,8 @@ export class DeviceWrapper { await this.clickOnElementAll(new CloseSettings(this)); } - public async processPermissions(locator: LocatorsInterface) { - const locatorConfig = locator.build(); + public async processPermissions(locator: LocatorsInterface | StrategyExtractionObj) { + const locatorConfig = locator instanceof LocatorsInterface ? locator.build() : locator; if (this.isAndroid()) { const permissions = await this.doesElementExist({ @@ -2610,17 +2607,35 @@ export class DeviceWrapper { throw new Error('CTAs must have 1-2 buttons'); } - // Find and check heading - const elHeading = await this.waitForTextElementToBePresent(new CTAHeading(this)); + // Fallback locators for Donate CTA on iOS (no accessibility IDs) + const headingFallback = { + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, + } as const; + const bodyFallback = { + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, + } as const; + const positiveButtonFallback = { + strategy: 'accessibility id', + selector: 'Donate', + } as const; + const negativeButtonFallback = { + strategy: 'accessibility id', + selector: 'Maybe Later', + } as const; + + // Find and check heading (with fallback for Donate CTA) + const elHeading = await this.findWithFallback(new CTAHeading(this), headingFallback); const actualHeading = await this.getTextFromElement(elHeading); this.assertTextMatches(actualHeading, heading, 'CTA heading'); - // Find and check body - const elBody = await this.waitForTextElementToBePresent(new CTABody(this)); + // Find and check body (with fallback for Donate CTA) + const elBody = await this.findWithFallback(new CTABody(this), bodyFallback); const actualBody = await this.getTextFromElement(elBody); this.assertTextMatches(actualBody, body, 'CTA body'); - // Check features if expected + // Check features if expected (Pro CTAs only) if (features) { for (let i = 0; i < features.length; i++) { const featureLocator = new CTAFeature(this, i); @@ -2630,15 +2645,15 @@ export class DeviceWrapper { } } - // Check buttons + // Check buttons (with fallback for Donate CTA) const positiveLocator = new CTAButtonPositive(this); - const elPositive = await this.waitForTextElementToBePresent(positiveLocator); + const elPositive = await this.findWithFallback(positiveLocator, positiveButtonFallback); const actualPositive = await this.getTextFromElement(elPositive); this.assertTextMatches(actualPositive, buttons[0], 'CTA positive button'); if (buttons.length === 2) { const negativeLocator = new CTAButtonNegative(this); - const elNegative = await this.waitForTextElementToBePresent(negativeLocator); + const elNegative = await this.findWithFallback(negativeLocator, negativeButtonFallback); const actualNegative = await this.getTextFromElement(elNegative); this.assertTextMatches(actualNegative, buttons[1], 'CTA negative button'); } @@ -2650,12 +2665,31 @@ export class DeviceWrapper { // This is the bare minimum of a CTA so we only check these // Features may or may not exist anyway, same goes for negative buttons - public async verifyNoCTAShows(): Promise { - await Promise.all([ - this.verifyElementNotPresent(new CTAHeading(this)), - this.verifyElementNotPresent(new CTABody(this)), - this.verifyElementNotPresent(new CTAButtonPositive(this)), - ]); + public async verifyNoCTAShows(ctaType?: CTAType): Promise { + // For Donate CTA on iOS, check the XPath selectors since accessibility IDs don't exist + if (ctaType === 'donate' && this.isIOS()) { + await Promise.all([ + this.verifyElementNotPresent({ + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, + }), + this.verifyElementNotPresent({ + strategy: 'xpath', + selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, + }), + this.verifyElementNotPresent({ + strategy: 'accessibility id', + selector: 'Donate', + }), + ]); + } else { + // For all other cases, use the standard CTA locators + await Promise.all([ + this.verifyElementNotPresent(new CTAHeading(this)), + this.verifyElementNotPresent(new CTABody(this)), + this.verifyElementNotPresent(new CTAButtonPositive(this)), + ]); + } } // Dismiss any CTA if it shows diff --git a/run/types/testing.ts b/run/types/testing.ts index 2aec099f1..e86d20d3b 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -183,6 +183,7 @@ export type AccessibilityId = | 'Allow' | 'Allow Access to All Photos' | 'Allow Full Access' + | 'Allow Paste' | 'Allow voice and video calls' | 'All Photos' | 'Answer call' @@ -234,6 +235,10 @@ export type AccessibilityId = | 'Copy URL' | 'Create account button' | 'Create group' + | 'cta-body' + | 'cta-button-negative' + | 'cta-button-positive' + | 'cta-heading' | 'Decline message request' | 'Delete' | 'Delete Contact' @@ -352,6 +357,7 @@ export type AccessibilityId = | 'open-survey-button' | 'Open' | 'Open URL' + | 'Paste' | 'Path' | 'Photo library' | 'Photos' @@ -424,6 +430,7 @@ export type AccessibilityId = | 'Your message request has been accepted.' | `${DISAPPEARING_TIMES} - Radio` | `${GROUPNAME}` + | `cta-feature-${number}` | `Disappear after ${DisappearActions} option`; export type Id = diff --git a/scripts/create_ios_simulators.ts b/scripts/create_ios_simulators.ts index 79a008534..051616aac 100644 --- a/scripts/create_ios_simulators.ts +++ b/scripts/create_ios_simulators.ts @@ -44,7 +44,7 @@ const DEVICE_CONFIG = { const MEDIA_ROOT = path.join('run', 'test', 'media'); const MEDIA_FILES = { - images: ['profile_picture.jpg', 'test_image.jpg'], + images: ['profile_picture.jpg', 'test_image.jpg', 'animated_profile_picture.gif'], videos: ['test_video.mp4'], pdfs: ['test_file.pdf'], }; From 5508edc96bedbbd5697b18113ccbea220a6d4d82 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 14:24:37 +1100 Subject: [PATCH 099/184] fix: new media picker locators --- run/types/DeviceWrapper.ts | 4 ++-- run/types/testing.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 48df6676a..c811d1d28 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1992,12 +1992,12 @@ export class DeviceWrapper { await sleepFor(500); await this.clickOnElementAll({ strategy: 'id', - selector: 'network.loki.messenger:id/mediapicker_folder_item_thumbnail', + selector: 'mediapicker-folder-item-thumbnail-0', }); await sleepFor(100); await this.clickOnElementAll({ strategy: 'id', - selector: 'network.loki.messenger:id/mediapicker_image_item_thumbnail', + selector: 'mediapicker-image-item-thumbnail-0', }); } await this.inputText(message, new MessageInput(this)); diff --git a/run/types/testing.ts b/run/types/testing.ts index e86d20d3b..08a0ef406 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -528,6 +528,8 @@ export type Id = | 'manage-admins-menu-option' | 'manage-members-menu-option' | 'Market cap amount' + | 'mediapicker-folder-item-thumbnail-0' + | 'mediapicker-image-item-thumbnail-0' | 'MeetingSE option' | 'Modal description' | 'Modal heading' @@ -546,8 +548,6 @@ export type Id = | 'network.loki.messenger:id/endCallButton' | 'network.loki.messenger:id/layout_emoji_container' | 'network.loki.messenger:id/linkPreviewView' - | 'network.loki.messenger:id/mediapicker_folder_item_thumbnail' - | 'network.loki.messenger:id/mediapicker_image_item_thumbnail' | 'network.loki.messenger:id/messageStatusTextView' | 'network.loki.messenger:id/openGroupTitleTextView' | 'network.loki.messenger:id/play_overlay' From 28d65c4515795303f4c5c1beebd84d04dcb1562c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 14:29:43 +1100 Subject: [PATCH 100/184] feat: recovery banner tests --- run/constants/community.ts | 24 +++- run/test/locators/conversation.ts | 6 +- run/test/specs/community_ban.spec.ts | 18 +-- run/test/specs/community_emoji_react.spec.ts | 15 +- run/test/specs/community_requests_off.spec.ts | 13 +- run/test/specs/community_requests_on.spec.ts | 19 +-- run/test/specs/community_tests_image.spec.ts | 8 +- run/test/specs/community_tests_join.spec.ts | 19 ++- .../disappearing_community_invite.spec.ts | 4 +- .../specs/linked_device_community_ban.spec.ts | 26 ++-- .../message_community_invitation.spec.ts | 10 +- run/test/specs/recovery_banner.spec.ts | 128 ++++++++++++++++++ run/types/allure.ts | 5 +- 13 files changed, 230 insertions(+), 65 deletions(-) create mode 100644 run/test/specs/recovery_banner.spec.ts diff --git a/run/constants/community.ts b/run/constants/community.ts index 5a575110c..4b2d3b36e 100644 --- a/run/constants/community.ts +++ b/run/constants/community.ts @@ -1,3 +1,21 @@ -export const testCommunityLink = `https://chat.lokinet.dev/testing-all-the-things?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17`; -export const testCommunityName = `Testing All The Things!`; -export const unresolvedTestcommunityName = 'testing-all-the-things'; +type CommunityConfig = { + link: string; + name: string; + roomName?: string; +}; + +export const communities: Record = { + testCommunity: { + link: 'https://chat.lokinet.dev/testing-all-the-things?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17', + name: 'Testing All The Things!', + roomName: 'testing-all-the-things', + }, + lokinetUpdates: { + link: 'https://open.getsession.org/lokinet-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Lokinet Updates', + }, + sessionNetworkUpdates: { + link: 'https://open.getsession.org/oxen-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Session Network Updates', + }, +}; diff --git a/run/test/locators/conversation.ts b/run/test/locators/conversation.ts index 2adeca44b..f132ce34d 100644 --- a/run/test/locators/conversation.ts +++ b/run/test/locators/conversation.ts @@ -1,6 +1,6 @@ import type { DeviceWrapper } from '../../types/DeviceWrapper'; -import { testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { StrategyExtractionObj } from '../../types/testing'; import { getAppDisplayName } from '../utils/devnet'; @@ -64,13 +64,13 @@ export class CommunityInvitation extends LocatorsInterface { return { strategy: 'id', selector: 'network.loki.messenger:id/openGroupTitleTextView', - text: testCommunityName, + text: communities.testCommunity.name, } as const; case 'ios': return { strategy: 'accessibility id', selector: 'Community invitation', - text: testCommunityName, + text: communities.testCommunity.name, } as const; } } diff --git a/run/test/specs/community_ban.spec.ts b/run/test/specs/community_ban.spec.ts index 94e289679..0495dfd41 100644 --- a/run/test/specs/community_ban.spec.ts +++ b/run/test/specs/community_ban.spec.ts @@ -1,6 +1,6 @@ import test, { type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; @@ -68,15 +68,15 @@ async function banUserCommunity(platform: SupportedPlatformsType, testInfo: Test }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -138,15 +138,15 @@ async function banAndDelete(platform: SupportedPlatformsType, testInfo: TestInfo }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index 06e612a7f..d17581a4b 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { EmojiReactsPill, FirstEmojiReact, MessageBody } from '../locators/conversation'; @@ -36,11 +36,16 @@ async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, test }); }); await Promise.all( - [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + [alice1, bob1].map(device => + joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name) + ) + ); + await test.step( + TestSteps.SEND.MESSAGE(alice.userName, communities.testCommunity.name), + async () => { + await alice1.sendMessage(message); + } ); - await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { - await alice1.sendMessage(message); - }); await test.step(TestSteps.SEND.EMOJI_REACT, async () => { await bob1.scrollToBottom(); await bob1.longPressMessage(new MessageBody(bob1, message)); diff --git a/run/test/specs/community_requests_off.spec.ts b/run/test/specs/community_requests_off.spec.ts index b1e04cbf2..9a72f2b0d 100644 --- a/run/test/specs/community_requests_off.spec.ts +++ b/run/test/specs/community_requests_off.spec.ts @@ -1,7 +1,7 @@ import { test, type TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { CommunityMessageAuthor, UPMMessageButton } from '../locators/conversation'; @@ -33,13 +33,16 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( [device1, device2].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); }) ); }); - await test.step(TestSteps.SEND.MESSAGE(USERNAME.BOB, testCommunityName), async () => { - await device2.sendMessage(message); - }); + await test.step( + TestSteps.SEND.MESSAGE(USERNAME.BOB, communities.testCommunity.name), + async () => { + await device2.sendMessage(message); + } + ); await device1.clickOnElementAll(new CommunityMessageAuthor(device1, message)); await test.step(`Verify the 'Message' button in the User Profile Modal is disabled`, async () => { // brief sleep to let the UI settle diff --git a/run/test/specs/community_requests_on.spec.ts b/run/test/specs/community_requests_on.spec.ts index 7862cd7d2..caefd37a8 100644 --- a/run/test/specs/community_requests_on.spec.ts +++ b/run/test/specs/community_requests_on.spec.ts @@ -1,7 +1,7 @@ import { test, type TestInfo } from '@playwright/test'; import { USERNAME } from '@session-foundation/qa-seeder'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { CloseSettings } from '../locators'; @@ -55,17 +55,20 @@ async function blindedMessageRequests(platform: SupportedPlatformsType, testInfo await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( [device1, device2].map(async device => { - await joinCommunity(device, testCommunityLink, testCommunityName); + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); }) ); }); - await test.step(TestSteps.SEND.MESSAGE(bob.userName, testCommunityName), async () => { - // brief sleep to let the UI settle - await sleepFor(1000); - await device2.sendMessage(message); - await device2.navigateBack(); - }); + await test.step( + TestSteps.SEND.MESSAGE(bob.userName, communities.testCommunity.name), + async () => { + // brief sleep to let the UI settle + await sleepFor(1000); + await device2.sendMessage(message); + await device2.navigateBack(); + } + ); await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { await device1.clickOnElementAll(new CommunityMessageAuthor(device1, message)); await sleepFor(500); // brief sleep to let the UI settle diff --git a/run/test/specs/community_tests_image.spec.ts b/run/test/specs/community_tests_image.spec.ts index 380a4d21f..6cc65028b 100644 --- a/run/test/specs/community_tests_image.spec.ts +++ b/run/test/specs/community_tests_image.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { MessageBody } from '../locators/conversation'; @@ -31,7 +31,9 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te const testImageMessage = `Image message + ${new Date().getTime()} - ${platform}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { await Promise.all( - [alice1, bob1].map(device => joinCommunity(device, testCommunityLink, testCommunityName)) + [alice1, bob1].map(device => + joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name) + ) ); }); await test.step(TestSteps.SEND.IMAGE, async () => { @@ -40,7 +42,7 @@ async function sendImageCommunity(platform: SupportedPlatformsType, testInfo: Te await test.step(TestSteps.VERIFY.MESSAGE_RECEIVED, async () => { await sleepFor(2000); // Give bob some time to receive the image await bob1.scrollToBottom(); - await bob1.onAndroid().trustAttachments(testCommunityName); + await bob1.onAndroid().trustAttachments(communities.testCommunity.name); await bob1.onAndroid().scrollToBottom(); // Trusting attachments scrolls the viewport up a bit so gotta scroll to bottom again await bob1.waitForTextElementToBePresent(new MessageBody(bob1, testImageMessage)); }); diff --git a/run/test/specs/community_tests_join.spec.ts b/run/test/specs/community_tests_join.spec.ts index cc24909dd..dd9bc8b4f 100644 --- a/run/test/specs/community_tests_join.spec.ts +++ b/run/test/specs/community_tests_join.spec.ts @@ -1,6 +1,6 @@ import { test, type TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationItem } from '../locators/home'; @@ -31,16 +31,21 @@ async function joinCommunityTest(platform: SupportedPlatformsType, testInfo: Tes }); const testMessage = `Test message + ${new Date().getTime()}`; await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await sleepFor(5000); }); - await test.step(TestSteps.SEND.MESSAGE(alice.userName, testCommunityName), async () => { - await alice1.scrollToBottom(); - await alice1.sendMessage(testMessage); - }); + await test.step( + TestSteps.SEND.MESSAGE(alice.userName, communities.testCommunity.name), + async () => { + await alice1.scrollToBottom(); + await alice1.sendMessage(testMessage); + } + ); await test.step(TestSteps.VERIFY.MESSAGE_SYNCED, async () => { // Has community synced to device 2? - await alice2.waitForTextElementToBePresent(new ConversationItem(alice2, testCommunityName)); + await alice2.waitForTextElementToBePresent( + new ConversationItem(alice2, communities.testCommunity.name) + ); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, alice2); diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index 620c027bf..a7bb7b148 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -1,6 +1,6 @@ import type { TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; import { InviteContactsMenuItem } from '../locators'; @@ -50,7 +50,7 @@ async function disappearingCommunityInviteMessage( await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); // await alice1.navigateBack(); await alice1.navigateBack(); - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(1000); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index 2cba89667..c09eb3a30 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -1,10 +1,6 @@ import test, { type TestInfo } from '@playwright/test'; -import { - testCommunityLink, - testCommunityName, - unresolvedTestcommunityName, -} from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; @@ -78,15 +74,15 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -107,7 +103,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.roomName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); await bob2.onIOS().waitForTextElementToBePresent({ strategy: 'xpath', @@ -152,15 +148,15 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te }); await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { const adminJoined = await alice1.doesElementExist( - new ConversationItem(alice1, testCommunityName) + new ConversationItem(alice1, communities.testCommunity.name) ); if (!adminJoined) { - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); } else { - await alice1.clickOnElementAll(new ConversationItem(alice1, testCommunityName)); + await alice1.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.name)); await alice1.scrollToBottom(); } - await joinCommunity(bob1, testCommunityLink, testCommunityName); + await joinCommunity(bob1, communities.testCommunity.link, communities.testCommunity.name); }); await test.step(TestSteps.SEND.MESSAGE('Bob', 'community'), async () => { await bob1.sendMessage(msg1); @@ -178,7 +174,7 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te }); await test.step(TestSteps.SETUP.RESTORE_ACCOUNT('Bob'), async () => { await restoreAccount(bob2, bob, 'bob2'); - await bob2.clickOnElementAll(new ConversationItem(alice1, unresolvedTestcommunityName)); // Since we're banned we don't get the "real" name + await bob2.clickOnElementAll(new ConversationItem(alice1, communities.testCommunity.roomName)); // Since we're banned we don't get the "real" name await bob2.waitForTextElementToBePresent(new EmptyConversation(bob2)); }); await test.step('Verify Bob cannot send messages in community on either device', async () => { diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index 7d622ae4d..20de78138 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -1,6 +1,6 @@ import type { TestInfo } from '@playwright/test'; -import { testCommunityLink, testCommunityName } from '../../constants/community'; +import { communities } from '../../constants/community'; import { tStripped } from '../../localizer/lib'; import { bothPlatformsIt } from '../../types/sessionIt'; import { InviteContactsMenuItem, JoinCommunityModalButton } from '../locators'; @@ -35,7 +35,7 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf // Join community on device 1 // Click on plus button await alice1.navigateBack(); - await joinCommunity(alice1, testCommunityLink, testCommunityName); + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); await alice1.clickOnElementAll(new ConversationSettings(alice1)); await sleepFor(500); await alice1.clickOnElementAll(new InviteContactsMenuItem(alice1)); @@ -45,10 +45,12 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf await bob1.clickOnElementAll(new CommunityInvitation(bob1)); await bob1.checkModalStrings( tStripped('communityJoin'), - tStripped('communityJoinDescription', { community_name: testCommunityName }) + tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }) ); await bob1.clickOnElementAll(new JoinCommunityModalButton(bob1)); await bob1.navigateBack(); - await bob1.waitForTextElementToBePresent(new ConversationItem(bob1, testCommunityName)); + await bob1.waitForTextElementToBePresent( + new ConversationItem(bob1, communities.testCommunity.name) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/recovery_banner.spec.ts b/run/test/specs/recovery_banner.spec.ts new file mode 100644 index 000000000..abf3c048f --- /dev/null +++ b/run/test/specs/recovery_banner.spec.ts @@ -0,0 +1,128 @@ +import { test, type TestInfo } from '@playwright/test'; +import { USERNAME } from '@session-foundation/qa-seeder'; + +import { communities } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { androidIt } from '../../types/sessionIt'; +import { ConversationItem, PlusButton } from '../locators/home'; +import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; +import { joinCommunity } from '../utils/community'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +androidIt({ + title: 'Recovery password banner only shows after >2 conversations', + risk: 'medium', + testCb: bannerShowsThreeConvos, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: + 'Verifies that the recovery password banner only shows after the user has at least three conversations.', +}); + +androidIt({ + title: 'Recovery password banner disappears after being opened', + risk: 'medium', + testCb: bannerDisappearsAfterOpened, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: 'Verifies that the recovery password banner disappears after first opened.', +}); + +androidIt({ + title: 'Recovery password banner persists with <3 conversations', + risk: 'medium', + testCb: bannerPersists, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Settings', + suite: 'Recovery Password', + }, + allureDescription: + 'Verifies that the recovery password banner does not disappear if the conversation count drops below 3', +}); + +async function bannerShouldNotshow(device: DeviceWrapper) { + await device.waitForTextElementToBePresent(new PlusButton(device)); + await device.verifyElementNotPresent(new RevealRecoveryPhraseButton(device)); + device.log('On home screen, banner did not appear'); +} + +async function bannerShouldShow(device: DeviceWrapper) { + await device.waitForTextElementToBePresent(new PlusButton(device)); + await device.waitForTextElementToBePresent(new RevealRecoveryPhraseButton(device)); + device.log('On home screen, banner appeared'); +} + +async function bannerShowsThreeConvos(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner only appears after the third', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await bannerShouldNotshow(device); + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function bannerDisappearsAfterOpened(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner does not reappear after being opened', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); + await device.waitForTextElementToBePresent(new RecoveryPhraseContainer(device)); + await device.navigateBack(); + await bannerShouldNotshow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function bannerPersists(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Create three conversations, verify banner persists after a conversation is deleted', async () => { + for (const community of Object.values(communities).slice(0, 3)) { + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } + await bannerShouldShow(device); + await device.longPressConversation(communities.testCommunity.name); + await device.clickOnElementAll({ strategy: 'accessibility id', selector: 'Leave' }); // Long press options + await device.clickOnElementAll({ strategy: 'accessibility id', selector: 'Leave' }); // Modal confirm + await device.verifyElementNotPresent( + new ConversationItem(device, communities.testCommunity.name) + ); + await bannerShouldShow(device); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} diff --git a/run/types/allure.ts b/run/types/allure.ts index a94054f41..801ae744f 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -31,7 +31,10 @@ export type AllureSuiteConfig = parent: 'Sending Messages'; suite: 'Emoji reacts' | 'Mentions' | 'Message types' | 'Performance' | 'Rules'; } - | { parent: 'Settings'; suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' } + | { + parent: 'Settings'; + suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' | 'Recovery Password'; + } | { parent: 'User Actions'; suite: From c0a0d834033a2d6cca385598808a53a14d956d9e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 14:45:19 +1100 Subject: [PATCH 101/184] fix: simplify animated element logic --- run/types/DeviceWrapper.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index c811d1d28..f3b16249e 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2719,9 +2719,6 @@ export class DeviceWrapper { const colors = new Set(); for (let i = 0; i < SAMPLE_SIZE; i++) { colors.add(await this.getElementPixelColor(args)); - if (i < SAMPLE_SIZE - 1) { - await sleepFor(300); - } } expect( colors.size, From 6cfe2b603c531aec49ef8fcee6e0371ae396e2d4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 18 Feb 2026 15:26:12 +1100 Subject: [PATCH 102/184] fix: simplify makeAccountPro signature --- run/test/specs/message_length.spec.ts | 6 +--- ...r_actions_animated_profile_picture.spec.ts | 5 +-- .../user_actions_share_to_session.spec.ts | 2 +- run/test/utils/mock_pro.ts | 33 ++++++++++--------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 21782420f..006cdc732 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -97,11 +97,7 @@ for (const testCase of messageLengthTestCases) { }); if (testCase.pro) { - const paymentProvider = platform === 'ios' ? 'apple' : 'google'; - await makeAccountPro({ - mnemonic: alice.recoveryPhrase, - provider: paymentProvider, - }); + await makeAccountPro({ user: alice, platform }); // Restart to notify app of Pro status change await forceStopAndRestart(device); await device.dismissCTA(); diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 286d7bd1d..18ff75bae 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -59,10 +59,7 @@ async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInf const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); - await makeAccountPro({ - mnemonic: alice.recoveryPhrase, - provider: 'google', - }); + await makeAccountPro({ user: alice, platform }); await forceStopAndRestart(device); await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { await device.uploadProfilePicture(true); 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 7cb2ad288..e5d6af1e5 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -31,7 +31,7 @@ bothPlatformsIt({ allureDescription: `Verifies that a user can share an image from the photo gallery to Session`, }); -// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. +// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. androidIt({ title: 'Share within Session', risk: 'medium', diff --git a/run/test/utils/mock_pro.ts b/run/test/utils/mock_pro.ts index b9b3f6a3e..cd8a2a2c1 100644 --- a/run/test/utils/mock_pro.ts +++ b/run/test/utils/mock_pro.ts @@ -10,10 +10,7 @@ * Usage: * import { makeAccountPro } from './mock_pro'; * - * await makeAccountPro({ - * mnemonic: 'word1 word2 ... word13', - * provider: 'google' // or 'apple' - * }); + * await makeAccountPro({ user: alice, platform }); * * In order for the changes to take effect in the clients it's best to force close and restart the app */ @@ -25,12 +22,14 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { PRO_BACKEND_URL } from '../../constants'; +import { User } from '../../types/testing'; +import { SupportedPlatformsType } from './open_app'; type PaymentProvider = 'apple' | 'google'; type MakeAccountProParams = { - mnemonic: string; - provider: PaymentProvider; + user: User; + platform: SupportedPlatformsType; dryRun?: boolean; // If true, build and print the request but don't send it }; @@ -84,7 +83,7 @@ function getWordlist(): string[] { return words; } -// Decodes a 13-word recovery phrase a 16-byte seed hex string. */ +// Decodes a 13-word recovery phrase to a 16-byte seed hex string. */ function mnemonicToSeedHex(mnemonic: string): string { const wordlist = getWordlist(); const n = wordlist.length; // 1626 @@ -326,14 +325,16 @@ async function addProPayment( // Registers a test account as a Pro subscriber against the dev backend. export async function makeAccountPro(params: MakeAccountProParams): Promise { - const { mnemonic, provider, dryRun = false } = params; + const { user, platform, dryRun = false } = params; + const mnemonic = user.recoveryPhrase; + const provider: PaymentProvider = platform === 'ios' ? 'apple' : 'google'; const seedHex = mnemonicToSeedHex(mnemonic); const masterKey = deriveProMasterKey(seedHex); const rotatingKey = generateRotatingKey(); // Build request const request = buildAddProPaymentRequest(masterKey, rotatingKey, provider); - console.log('\nRequest body:'); - console.log(JSON.stringify(request, null, 2)); + console.log(`\nRequest body: + ${JSON.stringify(request, null, 2)}`); if (dryRun) { console.log('\nDRY RUN - Request not sent'); @@ -359,22 +360,22 @@ if (require.main === module) { if (args.length < 2) { console.error( - 'Usage: npx ts-node run/test/utils/mock_pro.ts [--dry-run]' + 'Usage: npx ts-node run/test/utils/mock_pro.ts [--dry-run]' ); - console.error('Example: npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." google'); + console.error('Example: npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." android'); console.error( - ' npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." apple --dry-run' + ' npx ts-node run/test/utils/mock_pro.ts "word1 word2 ..." ios --dry-run' ); process.exit(1); } const dryRun = args.includes('--dry-run'); const filteredArgs = args.filter(a => a !== '--dry-run'); - const [mnemonic, provider] = filteredArgs; + const [mnemonic, platform] = filteredArgs; makeAccountPro({ - mnemonic, - provider: provider as PaymentProvider, + user: { userName: '' as any, accountID: '', recoveryPhrase: mnemonic }, + platform: platform as SupportedPlatformsType, dryRun, }) .then(() => process.exit(0)) From 4f6717d6a5e84581a6b71fc99871fc123faaed30 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Feb 2026 09:54:40 +1100 Subject: [PATCH 103/184] chore: update simulators --- ci-simulators.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ci-simulators.json b/ci-simulators.json index 57286b112..3c668df11 100644 --- a/ci-simulators.json +++ b/ci-simulators.json @@ -1,62 +1,62 @@ [ { "name": "Auto-17-0", - "udid": "319EF767-3947-47BD-8403-A2810A900352", + "udid": "38A6770A-89CB-48AE-AB76-4B7253887040", "wdaPort": 1253 }, { "name": "Auto-17-1", - "udid": "22C2F845-C59B-4AEC-9869-35AE2F3B78DE", + "udid": "178068B0-84DC-4FD0-A69B-90EF9C3B6B22", "wdaPort": 1254 }, { "name": "Auto-17-2", - "udid": "6ABF1BCD-7D4E-4ED3-998C-3B0A0F630658", + "udid": "84141341-BB1C-45B1-9F23-28BAC850B1B7", "wdaPort": 1255 }, { "name": "Auto-17-3", - "udid": "116D725B-DB87-4D76-900D-43510C5CA3BD", + "udid": "82999D57-1270-4ACF-9D5A-DC3A159577F7", "wdaPort": 1256 }, { "name": "Auto-17-4", - "udid": "AC308EC5-C18A-4B94-8780-18B3C8BE202D", + "udid": "12895DA0-F007-4AFB-BA06-7C201A482821", "wdaPort": 1257 }, { "name": "Auto-17-5", - "udid": "D0982657-C25C-45C7-9339-A1DA7B5C8F25", + "udid": "0EADC288-C020-4BBA-BA81-5E0936E2B424", "wdaPort": 1258 }, { "name": "Auto-17-6", - "udid": "2ADA6ACA-CE46-403F-AD91-53432A334C80", + "udid": "AF1A03EC-C335-4906-9C2C-F9BFBC3489A5", "wdaPort": 1259 }, { "name": "Auto-17-7", - "udid": "74511AFF-9687-4100-86EF-923A9B7537B4", + "udid": "EB9E97BF-15DB-4FCC-9B8D-EB6F336315B0", "wdaPort": 1260 }, { "name": "Auto-17-8", - "udid": "E81674B2-E37A-42CA-B7FB-206B9BCC53D6", + "udid": "FAE61862-F057-4C85-BB68-7CD204BA1648", "wdaPort": 1261 }, { "name": "Auto-17-9", - "udid": "7FA71A81-BD37-4CB5-B45A-E1615E697E1F", + "udid": "68BA9CC1-148D-49B6-9C30-28F86F2F16CA", "wdaPort": 1262 }, { "name": "Auto-17-10", - "udid": "A03EA1A6-F02C-4BC0-92FB-1A3FBCB422A2", + "udid": "A7AB2F64-127D-4696-A11B-F3A9F773739F", "wdaPort": 1263 }, { "name": "Auto-17-11", - "udid": "43E9330C-F024-4684-8B1C-6F1355867550", + "udid": "91CCA847-BFAA-459C-A368-4C13BE71ED02", "wdaPort": 1264 } ] \ No newline at end of file From 4f799bb8edca22c4903f47b0de774b79e39aec9e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Feb 2026 09:54:40 +1100 Subject: [PATCH 104/184] chore: update simulators --- ci-simulators.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ci-simulators.json b/ci-simulators.json index 57286b112..3c668df11 100644 --- a/ci-simulators.json +++ b/ci-simulators.json @@ -1,62 +1,62 @@ [ { "name": "Auto-17-0", - "udid": "319EF767-3947-47BD-8403-A2810A900352", + "udid": "38A6770A-89CB-48AE-AB76-4B7253887040", "wdaPort": 1253 }, { "name": "Auto-17-1", - "udid": "22C2F845-C59B-4AEC-9869-35AE2F3B78DE", + "udid": "178068B0-84DC-4FD0-A69B-90EF9C3B6B22", "wdaPort": 1254 }, { "name": "Auto-17-2", - "udid": "6ABF1BCD-7D4E-4ED3-998C-3B0A0F630658", + "udid": "84141341-BB1C-45B1-9F23-28BAC850B1B7", "wdaPort": 1255 }, { "name": "Auto-17-3", - "udid": "116D725B-DB87-4D76-900D-43510C5CA3BD", + "udid": "82999D57-1270-4ACF-9D5A-DC3A159577F7", "wdaPort": 1256 }, { "name": "Auto-17-4", - "udid": "AC308EC5-C18A-4B94-8780-18B3C8BE202D", + "udid": "12895DA0-F007-4AFB-BA06-7C201A482821", "wdaPort": 1257 }, { "name": "Auto-17-5", - "udid": "D0982657-C25C-45C7-9339-A1DA7B5C8F25", + "udid": "0EADC288-C020-4BBA-BA81-5E0936E2B424", "wdaPort": 1258 }, { "name": "Auto-17-6", - "udid": "2ADA6ACA-CE46-403F-AD91-53432A334C80", + "udid": "AF1A03EC-C335-4906-9C2C-F9BFBC3489A5", "wdaPort": 1259 }, { "name": "Auto-17-7", - "udid": "74511AFF-9687-4100-86EF-923A9B7537B4", + "udid": "EB9E97BF-15DB-4FCC-9B8D-EB6F336315B0", "wdaPort": 1260 }, { "name": "Auto-17-8", - "udid": "E81674B2-E37A-42CA-B7FB-206B9BCC53D6", + "udid": "FAE61862-F057-4C85-BB68-7CD204BA1648", "wdaPort": 1261 }, { "name": "Auto-17-9", - "udid": "7FA71A81-BD37-4CB5-B45A-E1615E697E1F", + "udid": "68BA9CC1-148D-49B6-9C30-28F86F2F16CA", "wdaPort": 1262 }, { "name": "Auto-17-10", - "udid": "A03EA1A6-F02C-4BC0-92FB-1A3FBCB422A2", + "udid": "A7AB2F64-127D-4696-A11B-F3A9F773739F", "wdaPort": 1263 }, { "name": "Auto-17-11", - "udid": "43E9330C-F024-4684-8B1C-6F1355867550", + "udid": "91CCA847-BFAA-459C-A368-4C13BE71ED02", "wdaPort": 1264 } ] \ No newline at end of file From 9646f39eafbadceff1c3cc973402d8dfdcab112b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Feb 2026 10:41:34 +1100 Subject: [PATCH 105/184] fix: remove donate cta workarounds on ios --- run/test/locators/global.ts | 8 --- run/test/specs/cta_donate_review.spec.ts | 10 +--- run/test/specs/cta_donate_time.spec.ts | 2 +- run/types/DeviceWrapper.ts | 76 ++++++------------------ 4 files changed, 23 insertions(+), 73 deletions(-) diff --git a/run/test/locators/global.ts b/run/test/locators/global.ts index 4edae485f..58d264af4 100644 --- a/run/test/locators/global.ts +++ b/run/test/locators/global.ts @@ -119,8 +119,6 @@ export class CTABody extends LocatorsInterface { } } -// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) -// See SES-4930 export class CTAButtonNegative extends LocatorsInterface { public build() { switch (this.platform) { @@ -139,8 +137,6 @@ export class CTAButtonNegative extends LocatorsInterface { } } -// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) -// See SES-4930 export class CTAButtonPositive extends LocatorsInterface { public build() { switch (this.platform) { @@ -159,8 +155,6 @@ export class CTAButtonPositive extends LocatorsInterface { } } -// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA doesn't have features -// See SES-4930 export class CTAFeature extends LocatorsInterface { private index: number; @@ -186,8 +180,6 @@ export class CTAFeature extends LocatorsInterface { } } -// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) -// See SES-4930 export class CTAHeading extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/cta_donate_review.spec.ts b/run/test/specs/cta_donate_review.spec.ts index b99ccc816..267cfa95b 100644 --- a/run/test/specs/cta_donate_review.spec.ts +++ b/run/test/specs/cta_donate_review.spec.ts @@ -10,7 +10,7 @@ import { ReviewPromptItsGreatButton } from '../locators/home'; import { PathMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; -import { forceStopAndRestart as forceStopAndRestartApp } from '../utils/utilities'; +import { forceStopAndRestart } from '../utils/utilities'; import { verifyPageScreenshot } from '../utils/verify_screenshots'; bothPlatformsIt({ @@ -41,7 +41,7 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await test.step('Dismiss review prompt and restart the app', async () => { await device.clickOnElementAll(new ReviewPromptItsGreatButton(device)); await device.clickOnElementAll(new CloseSettings(device)); - await forceStopAndRestartApp(device); + await forceStopAndRestart(device); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Donate CTA'), async () => { await device.checkCTA('donate'); @@ -51,11 +51,7 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await verifyPageScreenshot(device, platform, 'cta_donate', testInfo); }); await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Open URL'), async () => { - const positiveButton = await device.findWithFallback(new CTAButtonPositive(device), { - strategy: 'accessibility id', - selector: 'Donate', - } as const); - await device.click(positiveButton.ELEMENT); + await device.clickOnElementAll(new CTAButtonPositive(device)); await device.checkModalStrings( tStripped('urlOpen'), tStripped('urlOpenDescription', { url: donateURL }) diff --git a/run/test/specs/cta_donate_time.spec.ts b/run/test/specs/cta_donate_time.spec.ts index ee5308ba6..c7e1f46c2 100644 --- a/run/test/specs/cta_donate_time.spec.ts +++ b/run/test/specs/cta_donate_time.spec.ts @@ -67,7 +67,7 @@ async function donateCTADoesntShowSixDaysAgo(platform: SupportedPlatformsType, t await test.step('Verify Donate CTA does not show', async () => { await Promise.all([ device.waitForTextElementToBePresent(new PlusButton(device)), - device.verifyNoCTAShows('donate'), + device.verifyNoCTAShows(), ]); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index f3b16249e..5e9015657 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2607,53 +2607,33 @@ export class DeviceWrapper { throw new Error('CTAs must have 1-2 buttons'); } - // Fallback locators for Donate CTA on iOS (no accessibility IDs) - const headingFallback = { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, - } as const; - const bodyFallback = { - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, - } as const; - const positiveButtonFallback = { - strategy: 'accessibility id', - selector: 'Donate', - } as const; - const negativeButtonFallback = { - strategy: 'accessibility id', - selector: 'Maybe Later', - } as const; - - // Find and check heading (with fallback for Donate CTA) - const elHeading = await this.findWithFallback(new CTAHeading(this), headingFallback); + // CTA heading + const elHeading = await this.waitForTextElementToBePresent(new CTAHeading(this)); const actualHeading = await this.getTextFromElement(elHeading); this.assertTextMatches(actualHeading, heading, 'CTA heading'); - // Find and check body (with fallback for Donate CTA) - const elBody = await this.findWithFallback(new CTABody(this), bodyFallback); + // CTA body + const elBody = await this.waitForTextElementToBePresent(new CTABody(this)); const actualBody = await this.getTextFromElement(elBody); this.assertTextMatches(actualBody, body, 'CTA body'); - // Check features if expected (Pro CTAs only) + // CTA features if present if (features) { for (let i = 0; i < features.length; i++) { - const featureLocator = new CTAFeature(this, i); - const elFeature = await this.waitForTextElementToBePresent(featureLocator); + const elFeature = await this.waitForTextElementToBePresent(new CTAFeature(this, i)); const actualFeature = await this.getTextFromElement(elFeature); - this.assertTextMatches(actualFeature, features[i], `CTA feature ${i}`); + this.assertTextMatches(actualFeature, features[i], `CTA feature ${i + 1}`); } } - // Check buttons (with fallback for Donate CTA) - const positiveLocator = new CTAButtonPositive(this); - const elPositive = await this.findWithFallback(positiveLocator, positiveButtonFallback); + // CTA positive button + const elPositive = await this.waitForTextElementToBePresent(new CTAButtonPositive(this)); const actualPositive = await this.getTextFromElement(elPositive); this.assertTextMatches(actualPositive, buttons[0], 'CTA positive button'); + // CTA negative button if present if (buttons.length === 2) { - const negativeLocator = new CTAButtonNegative(this); - const elNegative = await this.findWithFallback(negativeLocator, negativeButtonFallback); + const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); const actualNegative = await this.getTextFromElement(elNegative); this.assertTextMatches(actualNegative, buttons[1], 'CTA negative button'); } @@ -2664,35 +2644,17 @@ export class DeviceWrapper { } // This is the bare minimum of a CTA so we only check these - // Features may or may not exist anyway, same goes for negative buttons - public async verifyNoCTAShows(ctaType?: CTAType): Promise { - // For Donate CTA on iOS, check the XPath selectors since accessibility IDs don't exist - if (ctaType === 'donate' && this.isIOS()) { - await Promise.all([ - this.verifyElementNotPresent({ - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Session Needs')]`, - }), - this.verifyElementNotPresent({ - strategy: 'xpath', - selector: `//XCUIElementTypeStaticText[starts-with(@name,'Powerful forces are trying to')]`, - }), - this.verifyElementNotPresent({ - strategy: 'accessibility id', - selector: 'Donate', - }), - ]); - } else { - // For all other cases, use the standard CTA locators - await Promise.all([ - this.verifyElementNotPresent(new CTAHeading(this)), - this.verifyElementNotPresent(new CTABody(this)), - this.verifyElementNotPresent(new CTAButtonPositive(this)), - ]); - } + public async verifyNoCTAShows(): Promise { + await Promise.all([ + this.verifyElementNotPresent(new CTAHeading(this)), + this.verifyElementNotPresent(new CTABody(this)), + this.verifyElementNotPresent(new CTAButtonPositive(this)), + ]); } // Dismiss any CTA if it shows + // Note that not every CTA has negative buttons but a vast majority of them do + // And the ones that show and block the automation are likely to be ones with negative button public async dismissCTA(): Promise { const hasCTAAppeared = await this.doesElementExist({ ...new CTAButtonNegative(this).build(), From 37be43eac4460f2ecf37ade52132059d84ba1043 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Feb 2026 15:05:23 +1100 Subject: [PATCH 106/184] feat: pro activated cta test --- run/screenshots/android/cta_pro_activated.png | 3 ++ ...r_actions_animated_profile_picture.spec.ts | 41 ++++++++++++++++++- run/types/DeviceWrapper.ts | 25 +++++------ run/types/allure.ts | 1 + run/{test/utils/check_cta.ts => types/cta.ts} | 21 +++++++--- run/types/testing.ts | 1 + 6 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 run/screenshots/android/cta_pro_activated.png rename run/{test/utils/check_cta.ts => types/cta.ts} (57%) diff --git a/run/screenshots/android/cta_pro_activated.png b/run/screenshots/android/cta_pro_activated.png new file mode 100644 index 000000000..c47bd6839 --- /dev/null +++ b/run/screenshots/android/cta_pro_activated.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35fbdb49f97531ef2cb25b3027846d03ec4dc3590a02be30057770da80db8a27 +size 428879 diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 18ff75bae..34bd4ff22 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -1,14 +1,16 @@ import { test, type TestInfo } from '@playwright/test'; +import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { PathMenuItem, UserAvatar } from '../locators/settings'; +import { PathMenuItem, UserAvatar, UserSettings } from '../locators/settings'; import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; import { forceStopAndRestart } from '../utils/utilities'; +import { verifyPageScreenshot } from '../utils/verify_screenshots'; bothPlatformsIt({ title: 'Upload animated profile picture (non Pro)', @@ -32,6 +34,16 @@ bothPlatformsIt({ }, }); +bothPlatformsIt({ + title: 'Pro Activated CTA', + risk: 'medium', + countOfDevicesNeeded: 1, + testCb: proActivatedCTA, + allureSuites: { + parent: 'Session Pro', + }, +}); + async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { const iosContext: IOSTestContext = { sessionProEnabled: 'true', @@ -50,6 +62,33 @@ async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: Test await closeApp(device); }); } +async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(device); + await test.step('Verify Pro Activated CTA', async () => { + await device.clickOnElementAll(new UserSettings(device)); + await device.clickOnElementAll(new UserAvatar(device)); + await device.clickOnElementAll({ + strategy: 'id', + selector: 'pro-badge-text', + text: tStripped('proAnimatedDisplayPictureModalDescription'), + }); + await verifyPageScreenshot(device, platform, 'cta_pro_activated', testInfo); + await device.checkCTA('alreadyActivated'); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { const iosContext: IOSTestContext = { sessionProEnabled: 'true', diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5e9015657..5583dad07 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -75,9 +75,9 @@ import { clickOnCoordinates, sleepFor } from '../test/utils'; import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; -import { CTAConfig, ctaConfigs, CTAType } from '../test/utils/check_cta'; import { SupportedPlatformsType } from '../test/utils/open_app'; import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; +import { CTAConfig, ctaConfigs, CTAType } from './cta'; import { AccessibilityId, Coordinates, @@ -2626,16 +2626,18 @@ export class DeviceWrapper { } } - // CTA positive button - const elPositive = await this.waitForTextElementToBePresent(new CTAButtonPositive(this)); - const actualPositive = await this.getTextFromElement(elPositive); - this.assertTextMatches(actualPositive, buttons[0], 'CTA positive button'); + /** + * buttons[0] = negative/dismiss (always present); + * buttons[1] = positive/action (optional) + */ + const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); + const actualNegative = await this.getTextFromElement(elNegative); + this.assertTextMatches(actualNegative, buttons[0], 'CTA negative button'); - // CTA negative button if present if (buttons.length === 2) { - const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); - const actualNegative = await this.getTextFromElement(elNegative); - this.assertTextMatches(actualNegative, buttons[1], 'CTA negative button'); + const elPositive = await this.waitForTextElementToBePresent(new CTAButtonPositive(this)); + const actualPositive = await this.getTextFromElement(elPositive); + this.assertTextMatches(actualPositive, buttons[1], 'CTA positive button'); } } @@ -2648,13 +2650,11 @@ export class DeviceWrapper { await Promise.all([ this.verifyElementNotPresent(new CTAHeading(this)), this.verifyElementNotPresent(new CTABody(this)), - this.verifyElementNotPresent(new CTAButtonPositive(this)), + this.verifyElementNotPresent(new CTAButtonNegative(this)), ]); } // Dismiss any CTA if it shows - // Note that not every CTA has negative buttons but a vast majority of them do - // And the ones that show and block the automation are likely to be ones with negative button public async dismissCTA(): Promise { const hasCTAAppeared = await this.doesElementExist({ ...new CTAButtonNegative(this).build(), @@ -2674,6 +2674,7 @@ export class DeviceWrapper { const pixelColor = await parseDataImage(base64image); return pixelColor; } + // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. // If the set contains more than 1 color it is likely animated. public async verifyElementIsAnimated(args: LocatorsInterface): Promise { diff --git a/run/types/allure.ts b/run/types/allure.ts index 801ae744f..5872894b7 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -31,6 +31,7 @@ export type AllureSuiteConfig = parent: 'Sending Messages'; suite: 'Emoji reacts' | 'Mentions' | 'Message types' | 'Performance' | 'Rules'; } + | { parent: 'Session Pro' } | { parent: 'Settings'; suite: 'App Disguise' | 'Community Message Requests' | 'Notifications' | 'Recovery Password'; diff --git a/run/test/utils/check_cta.ts b/run/types/cta.ts similarity index 57% rename from run/test/utils/check_cta.ts rename to run/types/cta.ts index 96cc65665..6e0aa59b1 100644 --- a/run/test/utils/check_cta.ts +++ b/run/types/cta.ts @@ -1,11 +1,15 @@ -import { tStripped } from '../../localizer/lib'; +import { tStripped } from '../localizer/lib'; -export type CTAType = 'animatedProfilePicture' | 'donate' | 'longerMessages'; +export type CTAType = 'alreadyActivated' | 'animatedProfilePicture' | 'donate' | 'longerMessages'; +/** + * buttons[0] is the negative/dismiss button (always present); + * buttons[1] is the optional positive/action button + */ export type CTAConfig = { heading: string; body: string; - buttons: string[]; + buttons: [string, string] | [string]; features?: string[]; }; @@ -13,12 +17,12 @@ export const ctaConfigs: Record = { donate: { heading: tStripped('donateSessionHelp'), body: tStripped('donateSessionDescription'), - buttons: [tStripped('donate'), tStripped('maybeLater')], + buttons: [tStripped('maybeLater'), tStripped('donate')], }, longerMessages: { heading: tStripped('upgradeTo'), body: tStripped('proCallToActionLongerMessages'), - buttons: [tStripped('theContinue'), tStripped('cancel')], + buttons: [tStripped('cancel'), tStripped('theContinue')], features: [ tStripped('proFeatureListLongerMessages'), tStripped('proFeatureListPinnedConversations'), @@ -28,11 +32,16 @@ export const ctaConfigs: Record = { animatedProfilePicture: { heading: tStripped('upgradeTo'), body: tStripped('proAnimatedDisplayPictureCallToActionDescription'), - buttons: [tStripped('theContinue'), tStripped('cancel')], + buttons: [tStripped('cancel'), tStripped('theContinue')], features: [ tStripped('proFeatureListAnimatedDisplayPicture'), tStripped('proFeatureListLongerMessages'), tStripped('proFeatureListLoadsMore'), ], }, + alreadyActivated: { + heading: tStripped('proActivated'), + body: tStripped('proAnimatedDisplayPicture'), + buttons: [tStripped('close')], + }, }; diff --git a/run/types/testing.ts b/run/types/testing.ts index 08a0ef406..5c6710d6e 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -634,6 +634,7 @@ export type ScreenshotFileNames = | 'conversation_alice' | 'conversation_bob' | 'cta_donate' + | 'cta_pro_activated' | 'landingpage_new_account' | 'landingpage_restore_account' | 'settings_appearance' From 9450df63f6d92af3dd587dddee36f71b089ecec1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Feb 2026 11:21:31 +1100 Subject: [PATCH 107/184] fix: add more defensive emulator health checks --- .env.sample | 3 - .github/workflows/android-regression.yml | 1 + .github/workflows/ios-regression.yml | 4 - .../user_actions_share_to_session.spec.ts | 2 +- run/test/utils/binaries.ts | 32 -------- run/test/utils/capabilities_android.ts | 42 +---------- run/test/utils/open_app.ts | 50 +++---------- scripts/emulator_health.ts | 75 ++++++++++--------- 8 files changed, 53 insertions(+), 156 deletions(-) diff --git a/.env.sample b/.env.sample index 74ad21dd2..430e46d50 100644 --- a/.env.sample +++ b/.env.sample @@ -2,10 +2,7 @@ ANDROID_APK=/home/yougotthis/Downloads/session-android-universal.apk IOS_APP_PATH_PREFIX=/home/yougotthis/Downloads/Session.app -SDK_MANAGER_FULL_PATH=/home/yougotthis/Android/Sdk/cmdline-tools/latest/bin/sdkmanager -AVD_MANAGER_FULL_PATH=/home/yougotthis/Android/Sdk/cmdline-tools/latest/bin/avdmanager EMULATOR_FULL_PATH=/home/yougotthis/Android/Sdk/emulator/emulator -ANDROID_SYSTEM_IMAGE="system-images;android-35;google_atd;x86_64" APPIUM_ADB_FULL_PATH=/home/yougotthis/Android/sdk/platform-tools/adb PRINT_TEST_LOGS='true' PRINT_ONGOING_TEST_LOGS = 1 diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index afbb72464..bd9bc2088 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -78,6 +78,7 @@ jobs: IOS_APP_PATH_PREFIX: '' ANDROID_APK: './extracted/session-android.apk' APPIUM_ADB_FULL_PATH: '/opt/android/platform-tools/adb' + EMULATOR_FULL_PATH: '/opt/android/emulator/emulator' ANDROID_SDK_ROOT: '/opt/android' PLAYWRIGHT_RETRIES_COUNT: ${{ github.event.inputs.PLAYWRIGHT_RETRIES_COUNT }} _TESTING: 1 # Always hide webdriver logs (@appium/support/ flag) diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 2814afcb2..743649447 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -86,10 +86,6 @@ jobs: PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) PLAYWRIGHT_WORKERS_COUNT: 3 # for iOS, this is the max we can have on our self-hosted runner - SDK_MANAGER_FULL_PATH: '' - AVD_MANAGER_FULL_PATH: '' - ANDROID_SYSTEM_IMAGE: '' - EMULATOR_FULL_PATH: '' SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} steps: 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 75cbcfef4..c05a44307 100644 --- a/run/test/specs/user_actions_share_to_session.spec.ts +++ b/run/test/specs/user_actions_share_to_session.spec.ts @@ -31,7 +31,7 @@ bothPlatformsIt({ allureDescription: `Verifies that a user can share an image from the photo gallery to Session`, }); -// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. +// On iOS the Share button just opens the regular share sheet, same as 'Share to Session' - no need to test separately. androidIt({ title: 'Share within Session', risk: 'medium', diff --git a/run/test/utils/binaries.ts b/run/test/utils/binaries.ts index a30b5adb3..b4a0d80bf 100644 --- a/run/test/utils/binaries.ts +++ b/run/test/utils/binaries.ts @@ -38,38 +38,6 @@ export const getEmulatorFullPath = () => { return fromEnv; }; -export const getAvdManagerFullPath = () => { - const fromEnv = process.env.AVD_MANAGER_FULL_PATH; - - if (!fromEnv) { - throw new Error('env variable `AVD_MANAGER_FULL_PATH` needs to be set'); - } - existsAndFileOrThrow(fromEnv, 'AVD_MANAGER_FULL_PATH'); - - return fromEnv; -}; - -export const getSdkManagerFullPath = () => { - const fromEnv = process.env.SDK_MANAGER_FULL_PATH; - - if (!fromEnv) { - throw new Error('env variable `SDK_MANAGER_FULL_PATH` needs to be set'); - } - - existsAndFileOrThrow(fromEnv, 'SDK_MANAGER_FULL_PATH'); - - return fromEnv; -}; - -export const getAndroidSystemImageToUse = () => { - const fromEnv = process.env.ANDROID_SYSTEM_IMAGE; - - if (!fromEnv) { - throw new Error('env variable `ANDROID_SYSTEM_IMAGE` needs to be set'); - } - - return fromEnv; -}; export const getRetriesCount = () => { const asNumber = toNumber(process.env.PLAYWRIGHT_RETRIES_COUNT); diff --git a/run/test/utils/capabilities_android.ts b/run/test/utils/capabilities_android.ts index 7a152e4bf..e7aa4807f 100644 --- a/run/test/utils/capabilities_android.ts +++ b/run/test/utils/capabilities_android.ts @@ -27,58 +27,20 @@ const sharedCapabilities: AppiumAndroidCapabilities & AppiumCapabilities = { 'appium:eventTimings': false, }; -const emulator1Udid = 'emulator-5554'; -const emulator2Udid = 'emulator-5556'; -const emulator3Udid = 'emulator-5558'; -const emulator4Udid = 'emulator-5560'; -const emulator5Udid = 'emulator-5562'; -const emulator6Udid = 'emulator-5564'; -const emulator7Udid = 'emulator-5566'; -const emulator8Udid = 'emulator-5568'; - -const udids = [ - emulator1Udid, - emulator2Udid, - emulator3Udid, - emulator4Udid, - emulator5Udid, - emulator6Udid, - emulator7Udid, - emulator8Udid, -]; +const udids = ['emulator-5554', 'emulator-5556', 'emulator-5558', 'emulator-5560']; const emulatorCapabilities: AppiumCapabilities[] = udids.map(udid => ({ ...sharedCapabilities, 'appium:udid': udid, })); -// Access individual capabilities like this -const emulatorCapabilities1 = emulatorCapabilities[0]; -const emulatorCapabilities2 = emulatorCapabilities[1]; -const emulatorCapabilities3 = emulatorCapabilities[2]; -const emulatorCapabilities4 = emulatorCapabilities[3]; -const emulatorCapabilities5 = emulatorCapabilities[4]; -const emulatorCapabilities6 = emulatorCapabilities[5]; -const emulatorCapabilities7 = emulatorCapabilities[6]; -const emulatorCapabilities8 = emulatorCapabilities[7]; - export const androidCapabilities = { sharedCapabilities, androidAppFullPath, }; function getAllCaps() { - const emulatorCaps = [ - emulatorCapabilities1, - emulatorCapabilities2, - emulatorCapabilities3, - emulatorCapabilities4, - emulatorCapabilities5, - emulatorCapabilities6, - emulatorCapabilities7, - emulatorCapabilities8, - ]; - return emulatorCaps; + return emulatorCapabilities; } export function getAndroidCapabilities( diff --git a/run/test/utils/open_app.ts b/run/test/utils/open_app.ts index 38655f111..fa2e37e83 100644 --- a/run/test/utils/open_app.ts +++ b/run/test/utils/open_app.ts @@ -5,14 +5,9 @@ import { XCUITestDriverOpts } from 'appium-xcuitest-driver/build/lib/driver'; import { DriverOpts } from 'appium/build/lib/appium'; import { compact } from 'lodash'; +import { recoverEmulator } from '../../../scripts/emulator_health'; import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { - getAdbFullPath, - getAndroidSystemImageToUse, - getDevicesPerTestCount, - getEmulatorFullPath, - getSdkManagerFullPath, -} from './binaries'; +import { getAdbFullPath, getDevicesPerTestCount } from './binaries'; import { androidAppPackage, getAndroidCapabilities, getAndroidUdid } from './capabilities_android'; import { CapabilitiesIndexType, @@ -23,7 +18,7 @@ import { import { cleanPermissions } from './permissions'; import { registerDevicesForTest } from './screenshot_helper'; import { sleepFor } from './sleep_for'; -import { isCI, runScriptAndLog } from './utilities'; +import { runScriptAndLog } from './utilities'; const APPIUM_PORT = 4728; @@ -158,31 +153,6 @@ export const openAppFourDevices = async ( return result; }; -async function createAndroidEmulator(emulatorName: string) { - if (isCI()) { - // on CI, emulators are created during the docker build step. - return emulatorName; - } - const installSystemImageCmd = `${getSdkManagerFullPath()} --install '${getAndroidSystemImageToUse()}'`; - console.warn(installSystemImageCmd); - await runScriptAndLog(installSystemImageCmd); - - const createCmd = `echo "no" | ${getSdkManagerFullPath()} create avd --name ${emulatorName} -k '${getAndroidSystemImageToUse()}' --force --skin pixel_5`; - console.info(createCmd); - await runScriptAndLog(createCmd); - return emulatorName; -} - -async function startAndroidEmulator(emulatorName: string) { - await runScriptAndLog(`echo "hw.lcd.density=440" >> ~/.android/avd/${emulatorName}.avd/config.ini - `); - const startEmulatorCmd = `${getEmulatorFullPath()} @${emulatorName}`; - console.info(`${startEmulatorCmd} & ; disown`); - await runScriptAndLog( - startEmulatorCmd // -netdelay none -no-snapshot -wipe-data - ); -} - async function isEmulatorRunning(emulatorName: string) { const failedWith = await runScriptAndLog( `${getAdbFullPath()} -s ${emulatorName} get-state;`, @@ -247,13 +217,15 @@ const openAndroidApp = async ( const emulatorAlreadyRunning = await isEmulatorRunning(targetName); console.info('emulatorAlreadyRunning', targetName, emulatorAlreadyRunning); if (!emulatorAlreadyRunning) { - if (process.env.CI) { - throw new Error( - `Emulator "${targetName}" is not running but it should have been started earlier.` - ); + if (process.env.CI === '1') { + // Emulator died mid-job — attempt recovery before failing the test. + // Each worker owns a fixed port range (determined by TEST_PARALLEL_INDEX), so + // parallel workers will never race to recover the same emulator. + const port = parseInt(targetName.replace('emulator-', '')); + await recoverEmulator((port - 5554) / 2 + 1); + } else { + throw new Error(`Emulator "${targetName}" is not running. Please start it manually.`); } - await createAndroidEmulator(targetName); - void startAndroidEmulator(targetName); } await waitForEmulatorToBeRunning(targetName); console.log(targetName, ' emulator booted'); diff --git a/scripts/emulator_health.ts b/scripts/emulator_health.ts index e967f8494..3a3fafb11 100644 --- a/scripts/emulator_health.ts +++ b/scripts/emulator_health.ts @@ -1,4 +1,5 @@ import { sleepFor } from '../run/test/utils'; +import { getAdbFullPath, getEmulatorFullPath } from '../run/test/utils/binaries'; import { runScriptAndLog } from '../run/test/utils/utilities'; const EMULATOR_CONFIG = { @@ -9,7 +10,7 @@ const EMULATOR_CONFIG = { } as const; async function getRunningEmulators(): Promise { - const output = await runScriptAndLog('adb devices'); + const output = await runScriptAndLog(`${getAdbFullPath()} devices`); return output .split('\n') .map(line => { @@ -34,9 +35,10 @@ async function getMissingEmulators(): Promise { async function waitForEmulatorBoot( emulatorNum: number, - timeoutMs: number = 30_0000 + timeoutMs: number = 300_000 ): Promise { const port = EMULATOR_CONFIG[emulatorNum as keyof typeof EMULATOR_CONFIG]; + const udid = `emulator-${port}`; const startTime = Date.now(); const maxAttempts = Math.floor(timeoutMs / 5_000); @@ -45,7 +47,7 @@ async function waitForEmulatorBoot( for (let i = 0; i < maxAttempts; i++) { try { const result = await runScriptAndLog( - `adb -s emulator-${port} shell getprop sys.boot_completed 2>/dev/null`, + `${getAdbFullPath()} -s ${udid} shell getprop sys.boot_completed 2>/dev/null`, false ); @@ -65,50 +67,49 @@ async function waitForEmulatorBoot( return false; } -async function restartMissingEmulators(): Promise { - const missing = await getMissingEmulators(); - - if (missing.length === 0) { - console.log('All emulators running'); - return; - } +export async function recoverEmulator(emulatorNum: number): Promise { + const port = EMULATOR_CONFIG[emulatorNum as keyof typeof EMULATOR_CONFIG]; + const udid = `emulator-${port}`; + const avdName = `emulator${emulatorNum}`; - console.log(`Missing emulators: ${missing.join(', ')}`); - console.log(`Restarting emulators: ${missing.join(', ')}`); + console.warn(`[Recovery] ${udid} not running — attempting to recover ${avdName}...`); - for (const num of missing) { - const port = EMULATOR_CONFIG[num as keyof typeof EMULATOR_CONFIG]; + // Kill any zombie process + try { + await runScriptAndLog(`${getAdbFullPath()} -s ${udid} emu kill`, false); + await sleepFor(2_000); + } catch { + // Already dead, that's fine + } - // Kill if zombie process - try { - await runScriptAndLog(`adb -s emulator-${port} emu kill`, false); - await sleepFor(2_000); - } catch { - // Already dead, that's fine - } + // Restart from snapshot (mirrors ci.sh start_with_snapshots) + const configFile = `$HOME/.android/avd/${avdName}.avd/emulator-user.ini`; + const windowX = 100 + (emulatorNum - 1) * 400; + await runScriptAndLog(`sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, false); - // Restart from snapshot (same as ci.sh start_with_snapshots) - const configFile = `$HOME/.android/avd/emulator${num}.avd/emulator-user.ini`; - const windowX = 100 + (num - 1) * 400; + await runScriptAndLog( + `DISPLAY=:0 nohup ${getEmulatorFullPath()} @${avdName} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load > /dev/null 2>&1 &`, + false + ); - await runScriptAndLog(`sed -i "s/^window.x.*/window.x=${windowX}/" ${configFile}`, false); + const booted = await waitForEmulatorBoot(emulatorNum); + if (!booted) { + throw new Error(`[Recovery] ${udid} failed to boot`); + } +} - await runScriptAndLog( - `DISPLAY=:0 nohup emulator @emulator${num} -gpu host -accel on -no-snapshot-save -snapshot plop.snapshot -force-snapshot-load > /dev/null 2>&1 &`, - false - ); +async function restartMissingEmulators(): Promise { + const missing = await getMissingEmulators(); - await sleepFor(5_000); + if (missing.length === 0) { + console.log('All emulators running'); + return; } - console.log(`\nWaiting for ${missing.length} emulator(s) to boot...`); - - const bootResults = await Promise.all(missing.map(num => waitForEmulatorBoot(num))); + console.log(`Missing emulators: ${missing.join(', ')} — recovering...`); - if (bootResults.every(result => result)) { - console.log(`\nEmulators restarted and booted successfully`); - } else { - console.log(`\nSome emulators failed to boot`); + const results = await Promise.allSettled(missing.map(num => recoverEmulator(num))); + if (results.some(r => r.status === 'rejected')) { throw new Error('Emulator recovery failed'); } } From 525691b983c3b32f0ce2184784f285f0d43a69f1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Feb 2026 11:26:29 +1100 Subject: [PATCH 108/184] chore: linting --- run/test/utils/binaries.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/run/test/utils/binaries.ts b/run/test/utils/binaries.ts index b4a0d80bf..796ff1f2e 100644 --- a/run/test/utils/binaries.ts +++ b/run/test/utils/binaries.ts @@ -38,7 +38,6 @@ export const getEmulatorFullPath = () => { return fromEnv; }; - export const getRetriesCount = () => { const asNumber = toNumber(process.env.PLAYWRIGHT_RETRIES_COUNT); return isFinite(asNumber) ? asNumber : 0; From c91d7605d443dfbc3e8c26651fe78d3cfaba2f10 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Feb 2026 16:20:34 +1100 Subject: [PATCH 109/184] refactor: translate stateuser output to user type --- .../specs/group_tests_add_accountid.spec.ts | 2 +- run/test/specs/upm_homescreen.spec.ts | 2 +- run/test/state_builder/index.ts | 93 +++++++++++++------ run/test/utils/get_account_id.ts | 12 +-- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/run/test/specs/group_tests_add_accountid.spec.ts b/run/test/specs/group_tests_add_accountid.spec.ts index 2b70d03f4..283da19a0 100644 --- a/run/test/specs/group_tests_add_accountid.spec.ts +++ b/run/test/specs/group_tests_add_accountid.spec.ts @@ -52,7 +52,7 @@ async function addAccountIDToGroup(platform: SupportedPlatformsType, testInfo: T const userD = await test.step(TestSteps.SETUP.NEW_USER, async () => { return newUser(unknown1, USERNAME.DRACULA); }); - const aliceTruncatedPubkey = truncatePubkey(alice.sessionId, platform); + const aliceTruncatedPubkey = truncatePubkey(alice.accountID, platform); const historicMsg = `Hello from ${alice.userName}`; const userDTruncatedPubkey = truncatePubkey(userD.accountID, platform); const userDMsg = `Hello from ${userD.userName}`; diff --git a/run/test/specs/upm_homescreen.spec.ts b/run/test/specs/upm_homescreen.spec.ts index 1ef5c7ee6..aad3222c8 100644 --- a/run/test/specs/upm_homescreen.spec.ts +++ b/run/test/specs/upm_homescreen.spec.ts @@ -48,7 +48,7 @@ async function upmHomeScreen(platform: SupportedPlatformsType, testInfo: TestInf }); const elText = await alice1.getTextFromElement(el); const normalized = elText.replace(/\s+/g, ''); // account id comes in two lines - const expected = bob.sessionId.trim(); + const expected = bob.accountID.trim(); if (normalized !== expected) { console.log(`Expected: ${expected} Observed: ${normalized}`); diff --git a/run/test/state_builder/index.ts b/run/test/state_builder/index.ts index e448a9012..f6a33901b 100644 --- a/run/test/state_builder/index.ts +++ b/run/test/state_builder/index.ts @@ -14,16 +14,26 @@ import { } from '@session-foundation/qa-seeder'; import type { DeviceWrapper } from '../../types/DeviceWrapper'; +import type { User } from '../../types/testing'; import { ConversationItem } from '../locators/home'; +import { IOSTestContext } from '../utils/capabilities_ios'; import { getNetworkTarget } from '../utils/devnet'; import { openAppMultipleDevices, type SupportedPlatformsType } from '../utils/open_app'; import { restoreAccountNoFallback } from '../utils/restore_account'; -type WithAlice = { alice: StateUser }; -type WithBob = { bob: StateUser }; -type WithCharlie = { charlie: StateUser }; -type WithDracula = { dracula: StateUser }; +function toUser(stateUser: StateUser): User { + return { + userName: stateUser.userName, + accountID: stateUser.sessionId, + recoveryPhrase: stateUser.seedPhrase, + }; +} + +type WithAlice = { alice: User }; +type WithBob = { bob: User }; +type WithCharlie = { charlie: User }; +type WithDracula = { dracula: User }; type WithFocusFriendsConvo = { focusFriendsConvo: boolean }; type WithFocusGroupConvo = { focusGroupConvo: boolean }; @@ -91,14 +101,16 @@ async function openAppsWithState m.seedPhrase); await linkDevices(result.devices, seedPhrases); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; const formattedDevices = { @@ -156,10 +170,12 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ groupName, focusGroupConvo, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 3; @@ -169,6 +185,7 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); @@ -177,9 +194,9 @@ export async function open_Alice1_Bob1_Charlie1_friends_group({ const seedPhrases = result.prebuilt.users.map(m => m.seedPhrase); await linkDevices(result.devices, seedPhrases); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; - const charlie = result.prebuilt.users[2]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; @@ -214,10 +231,12 @@ export async function open_Alice2_Bob1_Charlie1_friends_group({ groupName, focusGroupConvo, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 4; @@ -227,15 +246,23 @@ export async function open_Alice2_Bob1_Charlie1_friends_group({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); result.devices[2].setDeviceIdentity('charlie1'); result.devices[3].setDeviceIdentity('alice2'); - const [alice, bob, charlie] = result.prebuilt.users; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); - const seedPhrases = [alice.seedPhrase, bob.seedPhrase, charlie.seedPhrase, alice.seedPhrase]; + const seedPhrases = [ + alice.recoveryPhrase, + bob.recoveryPhrase, + charlie.recoveryPhrase, + alice.recoveryPhrase, + ]; await linkDevices(result.devices, seedPhrases); const [alice1, bob1, charlie1, alice2] = result.devices; @@ -275,10 +302,12 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ groupName, focusGroupConvo = true, testInfo, + iOSContext, }: WithPlatform & WithFocusGroupConvo & { groupName: string; testInfo: TestInfo; + iOSContext?: IOSTestContext; }) { const stateToBuildKey = '3friendsInGroup'; const appsToOpen = 4; @@ -288,6 +317,7 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ stateToBuildKey, groupName, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); @@ -308,9 +338,9 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ charlie1, unknown1: result.devices[3], // not assigned yet }; - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; - const charlie = result.prebuilt.users[2]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); + const charlie = toUser(result.prebuilt.users[2]); const formattedUsers: WithUsers<3> = { alice, bob, @@ -330,7 +360,11 @@ export async function open_Alice1_Bob1_Charlie1_Unknown1({ }; } -export async function open_Alice2({ platform, testInfo }: WithPlatform & { testInfo: TestInfo }) { +export async function open_Alice2({ + platform, + testInfo, + iOSContext, +}: WithPlatform & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const prebuiltStateKey = '1user'; const appsToOpen = 2; const result = await openAppsWithState({ @@ -339,15 +373,16 @@ export async function open_Alice2({ platform, testInfo }: WithPlatform & { testI stateToBuildKey: prebuiltStateKey, groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('alice2'); // we want the first user to have the first 2 devices linked - const alice = result.prebuilt.users[0]; + const alice = toUser(result.prebuilt.users[0]); const alice1 = result.devices[0]; const alice2 = result.devices[1]; - const seedPhrases = [alice.seedPhrase, alice.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, alice.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const formattedUsers: WithUsers<1> = { @@ -370,7 +405,8 @@ export async function open_Alice2({ platform, testInfo }: WithPlatform & { testI export async function open_Alice1_bob1_notfriends({ platform, testInfo, -}: WithPlatform & { testInfo: TestInfo }) { + iOSContext, +}: WithPlatform & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const appsToOpen = 2; const result = await openAppsWithState({ platform, @@ -378,16 +414,17 @@ export async function open_Alice1_bob1_notfriends({ stateToBuildKey: '2users', groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('bob1'); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); const alice1 = result.devices[0]; const bob1 = result.devices[1]; - const seedPhrases = [alice.seedPhrase, bob.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, bob.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const formattedUsers: WithUsers<2> = { @@ -408,7 +445,8 @@ export async function open_Alice2_Bob1_friends({ platform, focusFriendsConvo, testInfo, -}: WithPlatform & WithFocusFriendsConvo & { testInfo: TestInfo }) { + iOSContext, +}: WithPlatform & WithFocusFriendsConvo & { testInfo: TestInfo; iOSContext?: IOSTestContext }) { const prebuiltStateKey = '2friends'; const appsToOpen = 3; const result = await openAppsWithState({ @@ -417,14 +455,15 @@ export async function open_Alice2_Bob1_friends({ stateToBuildKey: prebuiltStateKey, groupName: undefined, testInfo, + iOSContext, }); result.devices[0].setDeviceIdentity('alice1'); result.devices[1].setDeviceIdentity('alice2'); result.devices[2].setDeviceIdentity('bob1'); - const alice = result.prebuilt.users[0]; - const bob = result.prebuilt.users[1]; + const alice = toUser(result.prebuilt.users[0]); + const bob = toUser(result.prebuilt.users[1]); // we want the first user to have the first 2 devices linked - const seedPhrases = [alice.seedPhrase, alice.seedPhrase, bob.seedPhrase]; + const seedPhrases = [alice.recoveryPhrase, alice.recoveryPhrase, bob.recoveryPhrase]; await linkDevices(result.devices, seedPhrases); const alice1 = result.devices[0]; diff --git a/run/test/utils/get_account_id.ts b/run/test/utils/get_account_id.ts index 31f1b3b18..1c9a7bd2a 100644 --- a/run/test/utils/get_account_id.ts +++ b/run/test/utils/get_account_id.ts @@ -1,16 +1,10 @@ -import { StateUser } from '@session-foundation/qa-seeder'; - import { User } from '../../types/testing'; import { SupportedPlatformsType } from './open_app'; -// Sorts users by pubkey hex (StateUser.sessionId from qa-seeder or User.accountID from local types) and returns usernames -export function sortByPubkey(...users: Array) { +// Sorts users by pubkey hex and returns their usernames +export function sortByPubkey(...users: Array) { return [...users] - .sort((a, b) => { - const aKey = 'accountID' in a ? a.accountID : String(a.sessionId); - const bKey = 'accountID' in b ? b.accountID : String(b.sessionId); - return aKey.localeCompare(bKey); - }) + .sort((a, b) => a.accountID.localeCompare(b.accountID)) .map(user => user.userName); } From 5f139c55a8da78f166ecd775e26b0438f2254a00 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Feb 2026 16:21:11 +1100 Subject: [PATCH 110/184] refactor: standardize describelocator --- run/types/DeviceWrapper.ts | 75 +++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5583dad07..0eb378eac 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -82,7 +82,6 @@ import { AccessibilityId, Coordinates, DISAPPEARING_TIMES, - Group, Id, InteractionPoints, Strategy, @@ -571,6 +570,16 @@ export class DeviceWrapper { return []; } + private resolveLocator(args: LocatorsInterface | (StrategyExtractionObj & { text?: string })): { + locator: StrategyExtractionObj; + description: string; + } { + const built = args instanceof LocatorsInterface ? args.build() : args; + const text = args instanceof LocatorsInterface ? undefined : args.text; + const locator = text ? { ...built, text } : built; + return { locator, description: describeLocator(locator) }; + } + /** * Attempts to find an element using a primary locator, and if not found, falls back to a secondary locator. * This is useful for supporting UI transitions (e.g., between legacy and Compose Android screens) where @@ -588,13 +597,10 @@ export class DeviceWrapper { fallbackLocator: LocatorsInterface | StrategyExtractionObj, maxWait: number = 3000 ): Promise { - const primary = - primaryLocator instanceof LocatorsInterface ? primaryLocator.build() : primaryLocator; - const fallback = - fallbackLocator instanceof LocatorsInterface ? fallbackLocator.build() : fallbackLocator; - - const primaryDescription = describeLocator(primary); - const fallbackDescription = describeLocator(fallback); + const { locator: primary, description: primaryDescription } = + this.resolveLocator(primaryLocator); + const { locator: fallback, description: fallbackDescription } = + this.resolveLocator(fallbackLocator); try { return await this.waitForTextElementToBePresent({ ...primary, maxWait, skipHealing: true }); @@ -774,26 +780,22 @@ export class DeviceWrapper { args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), options?: { offset?: Coordinates } ): Promise { - const { text, maxWait = 10_000 } = args; - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { maxWait = 10_000 } = args; + const { locator, description } = this.resolveLocator(args); - // Merge text if provided - const finalLocator = text ? { ...locator, text } : locator; - - const displayText = describeLocator(finalLocator); - this.log(`Attempting long press on ${displayText}`); + this.log(`Attempting long press on ${description}`); await this.pollUntil( async () => { // Find the message - this.log(`Looking for: ${JSON.stringify(finalLocator)}`); + this.log(`Looking for: ${JSON.stringify(locator)}`); const el = await this.waitForTextElementToBePresent({ - ...finalLocator, + ...locator, maxWait: 1_000, }); if (!el) { - return { success: false, error: `Message not found: ${displayText}` }; + return { success: false, error: `Message not found: ${description}` }; } if (options?.offset) { this.log(`Offsetting long press by x=${options?.offset?.x}, y=${options?.offset?.y}`); @@ -815,7 +817,7 @@ export class DeviceWrapper { return { success: false, - error: `Long press didn't show context menu for ${displayText}`, + error: `Long press didn't show context menu for ${description}`, }; }, { @@ -1253,7 +1255,7 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const maxWait = args.maxWait || 2_000; // Wait for any transitions to complete @@ -1261,8 +1263,6 @@ export class DeviceWrapper { const element = await this.findElementQuietly(locator, args.text); - const description = describeLocator({ ...locator, text: args.text }); - if (element) { // Elements can disappear in the GUI but still be present in the DOM let isVisible: boolean; @@ -1307,13 +1307,11 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const text = args.text; const initialMaxWait = args.initialMaxWait ?? 10_000; const maxWait = args.maxWait ?? 30_000; - const description = describeLocator({ ...locator, text: args.text }); - // Track total time from start - disappearing timers begin on send, not on display const functionStartTime = Date.now(); // Phase 1: Wait for element to appear @@ -1358,13 +1356,11 @@ export class DeviceWrapper { maxWait?: number; } & (LocatorsInterface | StrategyExtractionObj) ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; + const { locator, description } = this.resolveLocator(args); const text = args.text; const initialMaxWait = args.initialMaxWait ?? 10_000; const maxWait = args.maxWait ?? 30_000; - const description = describeLocator({ ...locator, text: args.text }); - // Phase 1: Wait for element to appear this.log(`Waiting for element with ${description} to be deleted...`); await this.waitForElementToAppear(locator, initialMaxWait, text); @@ -1734,8 +1730,7 @@ export class DeviceWrapper { expectedColor: string, tolerance?: number ): Promise { - const locator = args instanceof LocatorsInterface ? args.build() : args; - const description = describeLocator({ ...locator, text: args.text }); + const { locator, description } = this.resolveLocator(args); this.log(`Waiting for ${description} to have color #${expectedColor}`); @@ -1820,14 +1815,6 @@ export class DeviceWrapper { await this.onAndroid().clickOnElementAll(new AcceptMessageRequestButton(this)); } - public async sendMessageTo(sender: User, receiver: Group | User) { - const message = `${sender.userName} to ${receiver.userName}`; - await this.clickOnElementAll(new ConversationItem(this, receiver.userName)); - this.log(`${sender.userName} + " sent message to ${receiver.userName}`); - await this.sendMessage(message); - this.log(`Message received by ${receiver.userName} from ${sender.userName}`); - return message; - } // TODO instead of blind sleeping, check presence of reply preview // Remove blind sleep from other tests that reply as well public async replyToMessage(user: Pick, body: string) { @@ -2666,7 +2653,9 @@ export class DeviceWrapper { } } - public async getElementPixelColor(args: LocatorsInterface): Promise { + public async getElementPixelColor( + args: LocatorsInterface | StrategyExtractionObj + ): Promise { // Wait for the element to be present const element = await this.waitForTextElementToBePresent(args); // Take a screenshot and return a hex color value @@ -2677,11 +2666,15 @@ export class DeviceWrapper { // Sample an element's centre pixel color SAMPLE_SIZE times to determine whether it is animated or not. // If the set contains more than 1 color it is likely animated. - public async verifyElementIsAnimated(args: LocatorsInterface): Promise { + public async verifyElementIsAnimated( + args: LocatorsInterface | StrategyExtractionObj + ): Promise { + const { locator, description } = this.resolveLocator(args); + this.log(`Checking if ${description} is animated`); const SAMPLE_SIZE = 3; const colors = new Set(); for (let i = 0; i < SAMPLE_SIZE; i++) { - colors.add(await this.getElementPixelColor(args)); + colors.add(await this.getElementPixelColor(locator)); } expect( colors.size, From 64ae1cdb583484009f6708628b0ae82281e678f4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Feb 2026 16:21:21 +1100 Subject: [PATCH 111/184] feat: add 2-device animated dp test --- ...r_actions_animated_profile_picture.spec.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 34bd4ff22..1f1e0153b 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -4,7 +4,11 @@ import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; +import { CloseSettings } from '../locators'; +import { ConversationSettings, MessageBody } from '../locators/conversation'; +import { ConversationItem } from '../locators/home'; import { PathMenuItem, UserAvatar, UserSettings } from '../locators/settings'; +import { open_Alice1_Bob1_friends } from '../state_builder'; import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; import { makeAccountPro } from '../utils/mock_pro'; @@ -14,7 +18,7 @@ import { verifyPageScreenshot } from '../utils/verify_screenshots'; bothPlatformsIt({ title: 'Upload animated profile picture (non Pro)', - risk: 'medium', + risk: 'high', countOfDevicesNeeded: 1, testCb: nonProAnimatedDP, allureSuites: { @@ -25,7 +29,7 @@ bothPlatformsIt({ bothPlatformsIt({ title: 'Upload animated profile picture (Pro)', - risk: 'medium', + risk: 'high', countOfDevicesNeeded: 1, testCb: proAnimatedDP, allureSuites: { @@ -36,7 +40,7 @@ bothPlatformsIt({ bothPlatformsIt({ title: 'Pro Activated CTA', - risk: 'medium', + risk: 'low', countOfDevicesNeeded: 1, testCb: proActivatedCTA, allureSuites: { @@ -44,6 +48,16 @@ bothPlatformsIt({ }, }); +bothPlatformsIt({ + title: 'Animated Profile Picture shows', + risk: 'high', + countOfDevicesNeeded: 2, + testCb: proAnimatedDPShows, + allureSuites: { + parent: 'Session Pro', + }, +}); + async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { const iosContext: IOSTestContext = { sessionProEnabled: 'true', @@ -110,3 +124,33 @@ async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInf await closeApp(device); }); } + +async function proAnimatedDPShows(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const { devices, prebuilt } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return await open_Alice1_Bob1_friends({ + platform, + focusFriendsConvo: false, + testInfo, + iOSContext: iosContext, + }); + }); + const { alice1, bob1 } = devices; + const { alice, bob } = prebuilt; + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(alice1); + await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { + await alice1.uploadProfilePicture(true); + }); + await alice1.clickOnElementAll(new CloseSettings(alice1)); + await alice1.clickOnElementAll(new ConversationItem(alice1, bob.userName)); + await alice1.sendMessage('Howdy'); + await bob1.clickOnElementAll(new ConversationItem(bob1, alice.userName)); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1, 'Howdy')); + await bob1.verifyElementIsAnimated(new ConversationSettings(bob1)); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} From 77566f0422baa0d0888a7458a909ec68e28a3b2a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 11:40:26 +1100 Subject: [PATCH 112/184] feat: log capture on test failure --- run/test/utils/device_registry.ts | 62 ++++++ ...eenshot_helper.ts => failure_artifacts.ts} | 192 +++++++++++------- run/test/utils/open_app.ts | 12 +- run/types/sessionIt.ts | 11 +- 4 files changed, 188 insertions(+), 89 deletions(-) create mode 100644 run/test/utils/device_registry.ts rename run/test/utils/{screenshot_helper.ts => failure_artifacts.ts} (57%) diff --git a/run/test/utils/device_registry.ts b/run/test/utils/device_registry.ts new file mode 100644 index 000000000..6e29ba6b7 --- /dev/null +++ b/run/test/utils/device_registry.ts @@ -0,0 +1,62 @@ +import type { TestInfo } from '@playwright/test'; + +import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { getAdbFullPath } from './binaries'; +import { androidAppPackage } from './capabilities_android'; +import { SupportedPlatformsType } from './open_app'; +import { runScriptAndLog } from './utilities'; + +export type LogContext = { + startMs: number; // epoch ms — iOS: compared against log file mtime; Android: derived to epoch seconds for adb -T + pid?: string | null; // Android only — null if pidof returned nothing (app not yet running or already dead) +}; + +export type DeviceContext = { + devices: DeviceWrapper[]; + platform: SupportedPlatformsType; + logCtxByUdid?: Map; +}; + +export const deviceRegistry = new Map(); + +export function registryKey(testInfo: TestInfo): string { + return `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; +} + +// Async because Android registration fetches per-device PID for scoped logcat on failure. +export async function registerDevicesForTest( + testInfo: TestInfo, + devices: DeviceWrapper[], + platform: SupportedPlatformsType +) { + const key = registryKey(testInfo); + // Throw if registry already has an entry — indicates a previous test didn't unregister properly + if (deviceRegistry.has(key)) { + throw new Error(`Device registry already contains entry for test "${testInfo.title}"`); + } + + const startMs = Date.now(); + const logCtxByUdid = new Map(); + + if (platform === 'android') { + await Promise.all( + devices.map(async device => { + const pidOutput = await runScriptAndLog( + `${getAdbFullPath()} -s ${device.udid} shell pidof ${androidAppPackage}` + ); + const pid = pidOutput.trim() || null; + logCtxByUdid.set(device.udid, { startMs, pid }); + }) + ); + } else if (platform === 'ios') { + for (const device of devices) { + logCtxByUdid.set(device.udid, { startMs }); + } + } + + deviceRegistry.set(key, { devices, platform, logCtxByUdid }); +} + +export function unregisterDevicesForTest(testInfo: TestInfo) { + deviceRegistry.delete(registryKey(testInfo)); +} diff --git a/run/test/utils/screenshot_helper.ts b/run/test/utils/failure_artifacts.ts similarity index 57% rename from run/test/utils/screenshot_helper.ts rename to run/test/utils/failure_artifacts.ts index 9116ff257..e0e8a98ab 100644 --- a/run/test/utils/screenshot_helper.ts +++ b/run/test/utils/failure_artifacts.ts @@ -1,43 +1,19 @@ import type { TestInfo } from '@playwright/test'; +import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import sharp from 'sharp'; import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { getAdbFullPath } from './binaries'; +import { iOSBundleId } from './capabilities_ios'; +import { deviceRegistry, LogContext, registryKey } from './device_registry'; import { SupportedPlatformsType } from './open_app'; +import { runScriptAndLog } from './utilities'; -// Screenshot context type -type ScreenshotContext = { - devices: DeviceWrapper[]; - testInfo: TestInfo; - platform: SupportedPlatformsType; -}; - -// Global registry to track devices for screenshot capture -const deviceRegistry = new Map(); - -// Register devices for a test -export function registerDevicesForTest( - testInfo: TestInfo, - devices: DeviceWrapper[], - platform: SupportedPlatformsType -) { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - // Throw if deviceRegistry already has an entry for this test - // Could indicate that previous test did not unregister properly - if (deviceRegistry.has(testId)) { - throw new Error(`Device registry already contains entry for test "${testInfo.title}"`); - } - - deviceRegistry.set(testId, { devices, testInfo, platform }); -} +// --- Screenshots --- -// Unregister devices after test -export function unregisterDevicesForTest(testInfo: TestInfo) { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - deviceRegistry.delete(testId); -} // Add device labels to screenshots (e.g. "Device: alice1") async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promise { const { width } = await sharp(screenshot).metadata(); @@ -50,11 +26,11 @@ async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promis - Device: ${deviceName} @@ -63,14 +39,7 @@ async function addDeviceLabel(screenshot: Buffer, device: DeviceWrapper): Promis // Composite label over screenshot return sharp(screenshot) - .composite([ - { - input: label, - top: 0, - left: 0, - blend: 'over', - }, - ]) + .composite([{ input: label, top: 0, left: 0, blend: 'over' }]) .png() .toBuffer(); } @@ -116,14 +85,7 @@ async function createComposite(screenshots: Buffer[]): Promise { const composites = screenshots.map((screenshot, index) => { const col = index % cols; const row = Math.floor(index / cols); - const x = col * (width + gap); - const y = row * (height + gap); - - return { - input: screenshot, - left: x, - top: y, - }; + return { input: screenshot, left: col * (width + gap), top: row * (height + gap) }; }); // Apply all screenshots to canvas @@ -132,8 +94,7 @@ async function createComposite(screenshots: Buffer[]): Promise { // Main screenshot capture function export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { - const testId = `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; - const context = deviceRegistry.get(testId); + const context = deviceRegistry.get(registryKey(testInfo)); if (!context || context.devices.length === 0) { console.log('No devices registered for screenshot capture'); @@ -143,30 +104,20 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { try { const base64 = await device.getScreenshot(); - return { - device, - base64, - success: true, - }; + return { device, base64, success: true }; } catch (error) { console.error(`Failed to capture from ${device.getDeviceIdentity()}:`, error); - return { - device, - base64: null, - success: false, - }; + return { device, base64: null, success: false }; } }) ); // Filter out failed captures const successfulCaptures = rawCaptures.filter(c => c.success && c.base64); - if (successfulCaptures.length === 0) { console.log('No screenshots captured successfully'); return; @@ -180,16 +131,19 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise => { - if (result.status === 'rejected') { - console.error(`Failed to process screenshot:`, result.reason); - return false; + .filter( + ( + result + ): result is PromiseFulfilledResult<{ device: DeviceWrapper; labeledBuffer: Buffer }> => { + if (result.status === 'rejected') { + console.error(`Failed to process screenshot:`, result.reason); + return false; + } + return true; } - return true; - }) + ) .map(result => result.value.labeledBuffer); if (screenshots.length === 0) { @@ -198,7 +152,6 @@ export async function captureScreenshotsOnFailure(testInfo: TestInfo): Promise { + if (platform === 'android') { + const startEpochSec = (logCtx.startMs / 1000).toFixed(3); + const parts = [ + `${getAdbFullPath()} -s ${device.udid} logcat -d -T ${startEpochSec}`, + ...(logCtx.pid ? [`--pid=${logCtx.pid}`] : []), + ]; + const output = await runScriptAndLog(parts.join(' ')); + return Buffer.from(output); + } + + if (platform === 'ios') { + const containerPath = execSync( + `xcrun simctl get_app_container ${device.udid} ${iOSBundleId} data`, + { encoding: 'utf8' } + ).trim(); + + const logsDir = path.join(containerPath, 'Library', 'Caches', 'Logs'); + + if (!fs.existsSync(logsDir)) { + console.log(`No logs directory found for ${device.getDeviceIdentity()}`); + return null; + } + + const logFiles = fs + .readdirSync(logsDir) + .filter(f => f.startsWith(iOSBundleId) && f.endsWith('.log')) + .map(f => ({ name: f, mtime: fs.statSync(path.join(logsDir, f)).mtimeMs })) + .filter(f => f.mtime >= logCtx.startMs) + .sort((a, b) => b.mtime - a.mtime); + + if (logFiles.length === 0) { + console.log(`No log files found after test start for ${device.getDeviceIdentity()}`); + return null; + } + + return Buffer.from(fs.readFileSync(path.join(logsDir, logFiles[0].name), 'utf8')); + } + + return null; +} + +const MAX_LOG_BYTES = 2 * 1024 * 1024; // 2 MB — tail beyond this to keep reports lean + +function tailBuffer(raw: Buffer): Buffer { + if (raw.length <= MAX_LOG_BYTES) return raw; + + const tail = raw.subarray(raw.length - MAX_LOG_BYTES); + // Advance past any partial line at the cut point + const firstNewline = tail.indexOf('\n'.charCodeAt(0)); + return firstNewline > 0 ? tail.subarray(firstNewline + 1) : tail; +} + +export async function captureLogsOnFailure(testInfo: TestInfo): Promise { + const context = deviceRegistry.get(registryKey(testInfo)); + + if (!context?.logCtxByUdid) { + return; + } + + await Promise.all( + context.devices.map(async device => { + const logCtx = context.logCtxByUdid!.get(device.udid); + if (!logCtx) return; + + try { + const raw = await collectLogBuffer(context.platform, device, logCtx); + if (!raw) return; + + const buffer = tailBuffer(raw); + const label = device.getDeviceIdentity(); + const truncated = raw.length !== buffer.length; + await testInfo.attach(`log-${label}`, { body: buffer, contentType: 'text/plain' }); + console.log( + `Log captured for ${label} (${buffer.length} bytes${truncated ? `, truncated from ${raw.length}` : ''})` + ); + } catch (error) { + console.error(`Failed to capture log for ${device.getDeviceIdentity()}:`, error); + } + }) + ); +} diff --git a/run/test/utils/open_app.ts b/run/test/utils/open_app.ts index bb2821db1..cbc9c96c3 100644 --- a/run/test/utils/open_app.ts +++ b/run/test/utils/open_app.ts @@ -16,8 +16,8 @@ import { iOSBundleId, IOSTestContext, } from './capabilities_ios'; +import { registerDevicesForTest } from './device_registry'; import { cleanPermissions } from './permissions'; -import { registerDevicesForTest } from './screenshot_helper'; import { sleepFor } from './sleep_for'; import { runScriptAndLog } from './utilities'; @@ -42,7 +42,7 @@ export const openAppMultipleDevices = async ( // Map the result to return only the device objects const devices = apps.map(app => app.device); - registerDevicesForTest(testInfo, devices, platform); + await registerDevicesForTest(testInfo, devices, platform); return devices; }; @@ -70,7 +70,7 @@ export const openAppOnPlatformSingleDevice = async ( }> => { const result = await openAppOnPlatform(platform, 0, testInfo, iOSContext); - registerDevicesForTest(testInfo, [result.device], platform); + await registerDevicesForTest(testInfo, [result.device], platform); return result; }; @@ -90,7 +90,7 @@ export const openAppTwoDevices = async ( const result = { device1: app1.device, device2: app2.device }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; @@ -116,7 +116,7 @@ export const openAppThreeDevices = async ( device3: app3.device, }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; @@ -145,7 +145,7 @@ export const openAppFourDevices = async ( device4: app4.device, }; - registerDevicesForTest(testInfo, Object.values(result), platform); + await registerDevicesForTest(testInfo, Object.values(result), platform); return result; }; diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index 0b1be85bb..e329b16be 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -5,12 +5,10 @@ import { omit } from 'lodash'; import type { AppCountPerTest } from '../test/state_builder'; import { setupAllureTestInfo } from '../test/utils/allure/allureHelpers'; +import { unregisterDevicesForTest } from '../test/utils/device_registry'; import { getNetworkTarget } from '../test/utils/devnet'; +import { captureLogsOnFailure, captureScreenshotsOnFailure } from '../test/utils/failure_artifacts'; import { SupportedPlatformsType } from '../test/utils/open_app'; -import { - captureScreenshotsOnFailure, - unregisterDevicesForTest, -} from '../test/utils/screenshot_helper'; import { AllureSuiteConfig } from './allure'; import { TestRisk } from './testing'; @@ -105,9 +103,10 @@ function mobileIt({ testInfo.status === 'timedOut' ) { await captureScreenshotsOnFailure(testInfo); + await captureLogsOnFailure(testInfo); } - } catch (screenshotError) { - console.error('Failed to capture screenshot:', screenshotError); + } catch (artifactError) { + console.error('Failed to capture failure artifacts:', artifactError); } try { From 086b814697b84c8a453652bec5fa5c325a078fa6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 11:52:49 +1100 Subject: [PATCH 113/184] chore: rename tests to fit artifact upload rules --- run/test/specs/recovery_banner.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/specs/recovery_banner.spec.ts b/run/test/specs/recovery_banner.spec.ts index abf3c048f..c71967f64 100644 --- a/run/test/specs/recovery_banner.spec.ts +++ b/run/test/specs/recovery_banner.spec.ts @@ -12,7 +12,7 @@ import { newUser } from '../utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; androidIt({ - title: 'Recovery password banner only shows after >2 conversations', + title: 'Recovery password banner only shows after 3 conversations', risk: 'medium', testCb: bannerShowsThreeConvos, countOfDevicesNeeded: 1, @@ -37,7 +37,7 @@ androidIt({ }); androidIt({ - title: 'Recovery password banner persists with <3 conversations', + title: 'Recovery password banner persists with less than 3 conversations', risk: 'medium', testCb: bannerPersists, countOfDevicesNeeded: 1, From 946f0f9b8fa4515b3d1ed563d6d39dff6aa08b5c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 12:08:12 +1100 Subject: [PATCH 114/184] feat: differentiate between device and runner logs in allure --- package.json | 3 ++- patches/allure-playwright@3.4.5.patch | 38 +++++++++++++++++++++++++++ pnpm-lock.yaml | 15 ++++++----- run/test/utils/failure_artifacts.ts | 2 +- 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 patches/allure-playwright@3.4.5.patch diff --git a/package.json b/package.json index 90d4a8914..7b4aa28d0 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "packageManager": "pnpm@10.28.1", "pnpm": { "patchedDependencies": { - "appium-uiautomator2-driver": "patches/appium-uiautomator2-driver.patch" + "appium-uiautomator2-driver": "patches/appium-uiautomator2-driver.patch", + "allure-playwright@3.4.5": "patches/allure-playwright@3.4.5.patch" }, "ignoredBuiltDependencies": [ "appium-ios-tuntap", diff --git a/patches/allure-playwright@3.4.5.patch b/patches/allure-playwright@3.4.5.patch new file mode 100644 index 000000000..41c3e6267 --- /dev/null +++ b/patches/allure-playwright@3.4.5.patch @@ -0,0 +1,38 @@ +diff --git a/dist/cjs/index.js b/dist/cjs/index.js +index e09edfdc7f8ddc07f8865a9df6a8895b62f7998c..8af3d392a9a70b971f8fdb0f88e4ed336c5719e0 100644 +--- a/dist/cjs/index.js ++++ b/dist/cjs/index.js +@@ -504,12 +504,12 @@ var AllureReporter = exports.AllureReporter = /*#__PURE__*/function () { + testResult.stage = _allureJsCommons.Stage.FINISHED; + }); + if (result.stdout.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stdout", Buffer.from((0, _sdk.stripAnsi)(result.stdout.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner logs", Buffer.from((0, _sdk.stripAnsi)(result.stdout.join("")), "utf-8"), { + contentType: _allureJsCommons.ContentType.TEXT + }); + } + if (result.stderr.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stderr", Buffer.from((0, _sdk.stripAnsi)(result.stderr.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner errors", Buffer.from((0, _sdk.stripAnsi)(result.stderr.join("")), "utf-8"), { + contentType: _allureJsCommons.ContentType.TEXT + }); + } +diff --git a/dist/esm/index.js b/dist/esm/index.js +index 84ab12679c08c2738e7d9fb53267f950c60cfb86..b55ef16e6900f444aee1944cfa286baa37e6369c 100644 +--- a/dist/esm/index.js ++++ b/dist/esm/index.js +@@ -486,12 +486,12 @@ export var AllureReporter = /*#__PURE__*/function () { + testResult.stage = Stage.FINISHED; + }); + if (result.stdout.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stdout", Buffer.from(stripAnsi(result.stdout.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner logs", Buffer.from(stripAnsi(result.stdout.join("")), "utf-8"), { + contentType: ContentType.TEXT + }); + } + if (result.stderr.length > 0) { +- this.allureRuntime.writeAttachment(testUuid, undefined, "stderr", Buffer.from(stripAnsi(result.stderr.join("")), "utf-8"), { ++ this.allureRuntime.writeAttachment(testUuid, undefined, "test runner errors", Buffer.from(stripAnsi(result.stderr.join("")), "utf-8"), { + contentType: ContentType.TEXT + }); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b1d65c0b..ce24ef4f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ overrides: tar-fs@>=3.0.0 <3.0.7: '>=3.0.7' patchedDependencies: + allure-playwright@3.4.5: + hash: 8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305 + path: patches/allure-playwright@3.4.5.patch appium-uiautomator2-driver: hash: 8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121 path: patches/appium-uiautomator2-driver.patch @@ -94,10 +97,10 @@ importers: version: 2.36.0 allure-js-commons: specifier: ^3.4.5 - version: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) + version: 3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) allure-playwright: specifier: ^3.4.5 - version: 3.4.5(@playwright/test@1.58.2) + version: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) eslint: specifier: ^10.0.0 version: 10.0.0 @@ -3786,16 +3789,16 @@ snapshots: allure-commandline@2.36.0: {} - allure-js-commons@3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)): + allure-js-commons@3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)): dependencies: md5: 2.3.0 optionalDependencies: - allure-playwright: 3.4.5(@playwright/test@1.58.2) + allure-playwright: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) - allure-playwright@3.4.5(@playwright/test@1.58.2): + allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2): dependencies: '@playwright/test': 1.58.2 - allure-js-commons: 3.4.5(allure-playwright@3.4.5(@playwright/test@1.58.2)) + allure-js-commons: 3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) ansi-regex@5.0.1: {} diff --git a/run/test/utils/failure_artifacts.ts b/run/test/utils/failure_artifacts.ts index e0e8a98ab..3fd772322 100644 --- a/run/test/utils/failure_artifacts.ts +++ b/run/test/utils/failure_artifacts.ts @@ -270,7 +270,7 @@ export async function captureLogsOnFailure(testInfo: TestInfo): Promise { const buffer = tailBuffer(raw); const label = device.getDeviceIdentity(); const truncated = raw.length !== buffer.length; - await testInfo.attach(`log-${label}`, { body: buffer, contentType: 'text/plain' }); + await testInfo.attach(`device-log-${label}`, { body: buffer, contentType: 'text/plain' }); console.log( `Log captured for ${label} (${buffer.length} bytes${truncated ? `, truncated from ${raw.length}` : ''})` ); From b8f64ef62c86cd3b1ef8a288f55ddc4e1f247c6b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 16:41:26 +1100 Subject: [PATCH 115/184] refactor: speed up tests --- run/test/specs/disappear_after_read.spec.ts | 7 +- run/test/specs/disappear_after_send.spec.ts | 7 +- .../specs/disappear_after_send_groups.spec.ts | 2 +- .../disappear_after_send_note_to_self.spec.ts | 6 +- .../disappear_after_send_off_1o1.spec.ts | 9 +- run/test/specs/disappearing_call.spec.ts | 4 +- .../disappearing_community_invite.spec.ts | 2 +- run/test/specs/disappearing_gif.spec.ts | 2 +- run/test/specs/disappearing_image.spec.ts | 2 +- run/test/specs/disappearing_link.spec.ts | 4 +- .../disappearing_messages_defaults.spec.ts | 126 ++++++++++++++++++ ...appearing_messages_follow_settings.spec.ts | 60 +++++++++ run/test/specs/disappearing_video.spec.ts | 2 +- run/test/specs/disappearing_voice.spec.ts | 2 +- .../group_disappearing_messages_gif.spec.ts | 2 +- .../group_disappearing_messages_image.spec.ts | 2 +- .../group_disappearing_messages_link.spec.ts | 2 +- .../group_disappearing_messages_video.spec.ts | 2 +- .../group_disappearing_messages_voice.spec.ts | 2 +- run/test/specs/group_tests_promote.spec.ts | 8 +- run/test/utils/create_group.ts | 31 ++--- run/test/utils/restore_account.ts | 1 - run/test/utils/set_disappearing_messages.ts | 33 +---- 23 files changed, 220 insertions(+), 98 deletions(-) create mode 100644 run/test/specs/disappearing_messages_defaults.spec.ts create mode 100644 run/test/specs/disappearing_messages_follow_settings.spec.ts diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index 1b9773eba..d80ac5507 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -41,12 +41,7 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te let sentTimestamp: number; // Click conversation options menu (three dots) await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); }); // Check control message is correct on device 2 await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index e1c6b6d70..780122c56 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -35,12 +35,7 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const maxWait = 35_000; // 30s plus buffer // Select disappearing messages option - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); // Get control message based on key from json file await checkDisappearingControlMessage( platform, diff --git a/run/test/specs/disappear_after_send_groups.spec.ts b/run/test/specs/disappear_after_send_groups.spec.ts index 0ad50a7e1..dc16293b0 100644 --- a/run/test/specs/disappear_after_send_groups.spec.ts +++ b/run/test/specs/disappear_after_send_groups.spec.ts @@ -41,7 +41,7 @@ async function disappearAfterSendGroups(platform: SupportedPlatformsType, testIn }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['Group', `Disappear after send option`, time]); + await setDisappearingMessage(alice1, ['Group', `Disappear after send option`, time]); }); await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { // Get correct control message for You setting disappearing messages 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 f5d77399d..42199065d 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 @@ -46,11 +46,7 @@ async function disappearAfterSendNoteToSelf(platform: SupportedPlatformsType, te }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { // Enable disappearing messages - await setDisappearingMessage(platform, device, [ - 'Note to Self', - 'Disappear after send option', - time, - ]); + await setDisappearingMessage(device, ['Note to Self', 'Disappear after send option', time]); await sleepFor(1000); await device.waitForControlMessageToBePresent( `You set messages to disappear ${time} after they have been ${controlMode}.` 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 fcef4670d..998d9688c 100644 --- a/run/test/specs/disappear_after_send_off_1o1.spec.ts +++ b/run/test/specs/disappear_after_send_off_1o1.spec.ts @@ -12,7 +12,6 @@ import { SetDisappearMessagesButton, } from '../locators/disappearing_messages'; import { open_Alice2_Bob1_friends } from '../state_builder'; -import { sleepFor } from '../utils'; import { checkDisappearingControlMessage } from '../utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; import { setDisappearingMessage } from '../utils/set_disappearing_messages'; @@ -40,12 +39,7 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn const controlMode: DisappearActions = 'sent'; const time = DISAPPEARING_TIMES.THIRTY_SECONDS; // Select disappearing messages option - await setDisappearingMessage( - platform, - alice1, - ['1:1', `Disappear after ${mode} option`, time], - bob1 - ); + await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); // Get control message based on key from json file await checkDisappearingControlMessage( platform, @@ -78,7 +72,6 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn ]); // Follow setting on device 2 await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); - await sleepFor(500); await bob1.checkModalStrings( tStripped('disappearingMessagesFollowSetting'), tStripped('disappearingMessagesFollowSettingOff') diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index ceba5b8d0..09ba62b0a 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -44,7 +44,7 @@ async function disappearingCallMessage1o1Ios(platform: SupportedPlatformsType, t focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); await alice1.clickOnElementAll(new CallButton(alice1)); // Alice turns on all calls perms necessary (without checking every modal string) await alice1.clickOnByAccessibilityID('Settings'); @@ -127,7 +127,7 @@ async function disappearingCallMessage1o1Android( focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); await alice1.clickOnElementAll(new CallButton(alice1)); // Alice turns on all calls perms necessary (without checking every modal string) await alice1.clickOnElementAll({ diff --git a/run/test/specs/disappearing_community_invite.spec.ts b/run/test/specs/disappearing_community_invite.spec.ts index a7bb7b148..d529f8546 100644 --- a/run/test/specs/disappearing_community_invite.spec.ts +++ b/run/test/specs/disappearing_community_invite.spec.ts @@ -47,7 +47,7 @@ async function disappearingCommunityInviteMessage( focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); // await alice1.navigateBack(); await alice1.navigateBack(); await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); diff --git a/run/test/specs/disappearing_gif.spec.ts b/run/test/specs/disappearing_gif.spec.ts index 812e47773..7a0a3853c 100644 --- a/run/test/specs/disappearing_gif.spec.ts +++ b/run/test/specs/disappearing_gif.spec.ts @@ -33,7 +33,7 @@ async function disappearingGifMessage1o1(platform: SupportedPlatformsType, testI focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendGIF(); await bob1.trustAttachments(USERNAME.ALICE); await Promise.all( diff --git a/run/test/specs/disappearing_image.spec.ts b/run/test/specs/disappearing_image.spec.ts index afa7d4313..cdd35573e 100644 --- a/run/test/specs/disappearing_image.spec.ts +++ b/run/test/specs/disappearing_image.spec.ts @@ -33,7 +33,7 @@ async function disappearingImageMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendImage(testMessage); await bob1.trustAttachments(alice.userName); if (platform === 'ios') { diff --git a/run/test/specs/disappearing_link.spec.ts b/run/test/specs/disappearing_link.spec.ts index adf01dc06..4ac26ff1b 100644 --- a/run/test/specs/disappearing_link.spec.ts +++ b/run/test/specs/disappearing_link.spec.ts @@ -51,7 +51,7 @@ async function disappearingLinkMessage1o1Ios(platform: SupportedPlatformsType, t }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); @@ -105,7 +105,7 @@ async function disappearingLinkMessage1o1Android( }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time]); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); diff --git a/run/test/specs/disappearing_messages_defaults.spec.ts b/run/test/specs/disappearing_messages_defaults.spec.ts new file mode 100644 index 000000000..a356de22e --- /dev/null +++ b/run/test/specs/disappearing_messages_defaults.spec.ts @@ -0,0 +1,126 @@ +import type { TestInfo } from '@playwright/test'; + +import { bothPlatformsIt } from '../../types/sessionIt'; +import { DISAPPEARING_TIMES, GROUPNAME, USERNAME } from '../../types/testing'; +import { ConversationSettings } from '../locators/conversation'; +import { + DisappearingMessagesMenuOption, + DisappearingMessagesTimerType, +} from '../locators/disappearing_messages'; +import { PlusButton } from '../locators/home'; +import { EnterAccountID, NewMessageOption, NextButton } from '../locators/start_conversation'; +import { + open_Alice1_Bob1_Charlie1_friends_group, + open_Alice1_Bob1_friends, +} from '../state_builder'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +bothPlatformsIt({ + title: 'Disappearing messages defaults 1:1', + risk: 'medium', + testCb: disappearingMessagesDefaults1o1, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer for each DM mode in a 1:1 conversation', +}); + +bothPlatformsIt({ + title: 'Disappearing messages defaults group', + risk: 'medium', + testCb: disappearingMessagesDefaultsGroup, + countOfDevicesNeeded: 3, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer in a group conversation', +}); + +bothPlatformsIt({ + title: 'Disappearing messages defaults note to self', + risk: 'medium', + testCb: disappearingMessagesDefaultsNoteToSelf, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: 'Verifies the default selected timer in Note to Self', +}); + +async function disappearingMessagesDefaults1o1( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { + devices: { alice1, bob1 }, + } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new DisappearingMessagesMenuOption(alice1)); + + // Disappear after read: default should be 12 hours + await alice1.clickOnElementAll( + new DisappearingMessagesTimerType(alice1, 'Disappear after read option') + ); + await alice1.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.TWELVE_HOURS); + + // Disappear after send: default should be 1 day + await alice1.clickOnElementAll( + new DisappearingMessagesTimerType(alice1, 'Disappear after send option') + ); + await alice1.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); + + await closeApp(alice1, bob1); +} + +async function disappearingMessagesDefaultsGroup( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const testGroupName: GROUPNAME = 'Testing disappearing messages'; + const { + devices: { alice1, bob1, charlie1 }, + } = await open_Alice1_Bob1_Charlie1_friends_group({ + platform, + groupName: testGroupName, + focusGroupConvo: true, + testInfo, + }); + + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await alice1.clickOnElementAll(new DisappearingMessagesMenuOption(alice1)); + + // Group defaults: disappear after send should be OFF + await alice1.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); + await alice1.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); + + await closeApp(alice1, bob1, charlie1); +} + +async function disappearingMessagesDefaultsNoteToSelf( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + const alice = await newUser(device, USERNAME.ALICE); + + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(alice.accountID, new EnterAccountID(device)); + await device.scrollDown(); + await device.clickOnElementAll(new NextButton(device)); + + await device.clickOnElementAll(new ConversationSettings(device)); + await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); + + // Note to Self defaults: disappear after send should be OFF + await device.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); + await device.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); + + await closeApp(device); +} diff --git a/run/test/specs/disappearing_messages_follow_settings.spec.ts b/run/test/specs/disappearing_messages_follow_settings.spec.ts new file mode 100644 index 000000000..49955f013 --- /dev/null +++ b/run/test/specs/disappearing_messages_follow_settings.spec.ts @@ -0,0 +1,60 @@ +import type { TestInfo } from '@playwright/test'; + +import { tStripped } from '../../localizer/lib'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { DISAPPEARING_TIMES } from '../../types/testing'; +import { + DisappearingMessagesSubtitle, + FollowSettingsButton, + SetModalButton, +} from '../locators/disappearing_messages'; +import { open_Alice1_Bob1_friends } from '../state_builder'; +import { closeApp, SupportedPlatformsType } from '../utils/open_app'; +import { setDisappearingMessage } from '../utils/set_disappearing_messages'; + +bothPlatformsIt({ + title: 'Disappearing messages follow setting 1:1', + risk: 'medium', + testCb: disappearingMessagesFollowSetting1o1, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'Disappearing Messages', + suite: 'Conversation Types', + }, + allureDescription: + 'Verifies that Bob sees the Follow Setting banner when Alice sets disappearing messages in a 1:1 conversation, and that following applies the setting to both sides', +}); + +const time = DISAPPEARING_TIMES.THIRTY_SECONDS; +const timerType = 'Disappear after send option'; + +async function disappearingMessagesFollowSetting1o1( + platform: SupportedPlatformsType, + testInfo: TestInfo +) { + const { + devices: { alice1, bob1 }, + } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); + + await setDisappearingMessage(alice1, ['1:1', timerType, time]); + + // Bob should see the follow settings banner after Alice sets DM + await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); + await bob1.checkModalStrings( + tStripped('disappearingMessagesFollowSetting'), + tStripped('disappearingMessagesFollowSettingOn', { + time, + disappearing_messages_type: 'sent', + }) + ); + await bob1.clickOnElementAll(new SetModalButton(bob1)); + + // Both should now show the DM subtitle + await Promise.all( + [alice1, bob1].map(device => + device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)) + ) + ); + + await closeApp(alice1, bob1); +} diff --git a/run/test/specs/disappearing_video.spec.ts b/run/test/specs/disappearing_video.spec.ts index 9c81c6d41..fa3639451 100644 --- a/run/test/specs/disappearing_video.spec.ts +++ b/run/test/specs/disappearing_video.spec.ts @@ -35,7 +35,7 @@ async function disappearingVideoMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); let sentTimestamp: number; if (platform === 'ios') { sentTimestamp = await alice1.onIOS().sendVideoiOS(testMessage); diff --git a/run/test/specs/disappearing_voice.spec.ts b/run/test/specs/disappearing_voice.spec.ts index ba6718a82..26c218904 100644 --- a/run/test/specs/disappearing_voice.spec.ts +++ b/run/test/specs/disappearing_voice.spec.ts @@ -32,7 +32,7 @@ async function disappearingVoiceMessage1o1(platform: SupportedPlatformsType, tes focusFriendsConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['1:1', timerType, time], bob1); + await setDisappearingMessage(alice1, ['1:1', timerType, time]); const sentTimestamp = await alice1.sendVoiceMessage(); await bob1.trustAttachments(alice.userName); await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_gif.spec.ts b/run/test/specs/group_disappearing_messages_gif.spec.ts index 9c9c70eef..cec83e9df 100644 --- a/run/test/specs/group_disappearing_messages_gif.spec.ts +++ b/run/test/specs/group_disappearing_messages_gif.spec.ts @@ -35,7 +35,7 @@ async function disappearingGifMessageGroup(platform: SupportedPlatformsType, tes focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); // Click on attachments button const sentTimestamp = await alice1.sendGIF(); await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_image.spec.ts b/run/test/specs/group_disappearing_messages_image.spec.ts index 47ffbddf6..762bced73 100644 --- a/run/test/specs/group_disappearing_messages_image.spec.ts +++ b/run/test/specs/group_disappearing_messages_image.spec.ts @@ -34,7 +34,7 @@ async function disappearingImageMessageGroup(platform: SupportedPlatformsType, t testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); const sentTimestamp = await alice1.sendImage(testMessage); if (platform === 'ios') { await Promise.all( diff --git a/run/test/specs/group_disappearing_messages_link.spec.ts b/run/test/specs/group_disappearing_messages_link.spec.ts index 226834583..ec9ef96f9 100644 --- a/run/test/specs/group_disappearing_messages_link.spec.ts +++ b/run/test/specs/group_disappearing_messages_link.spec.ts @@ -47,7 +47,7 @@ async function disappearingLinkMessageGroup(platform: SupportedPlatformsType, te }); }); await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); }); await test.step(TestSteps.SEND.LINK, async () => { await alice1.inputText(testLink, new MessageInput(alice1)); diff --git a/run/test/specs/group_disappearing_messages_video.spec.ts b/run/test/specs/group_disappearing_messages_video.spec.ts index 20d3d048c..1121e05a5 100644 --- a/run/test/specs/group_disappearing_messages_video.spec.ts +++ b/run/test/specs/group_disappearing_messages_video.spec.ts @@ -36,7 +36,7 @@ async function disappearingVideoMessageGroup(platform: SupportedPlatformsType, t focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); let sentTimestamp: number; if (platform === 'ios') { sentTimestamp = await alice1.sendVideoiOS(testMessage); diff --git a/run/test/specs/group_disappearing_messages_voice.spec.ts b/run/test/specs/group_disappearing_messages_voice.spec.ts index 25862d0a7..b933dcc83 100644 --- a/run/test/specs/group_disappearing_messages_voice.spec.ts +++ b/run/test/specs/group_disappearing_messages_voice.spec.ts @@ -31,7 +31,7 @@ async function disappearingVoiceMessageGroup(platform: SupportedPlatformsType, t focusGroupConvo: true, testInfo, }); - await setDisappearingMessage(platform, alice1, ['Group', timerType, time]); + await setDisappearingMessage(alice1, ['Group', timerType, time]); const sentTimestamp = await alice1.sendVoiceMessage(); await Promise.all( [bob1, charlie1].map(device => device.onAndroid().trustAttachments(testGroupName)) diff --git a/run/test/specs/group_tests_promote.spec.ts b/run/test/specs/group_tests_promote.spec.ts index 0420b6885..5ad2cf797 100644 --- a/run/test/specs/group_tests_promote.spec.ts +++ b/run/test/specs/group_tests_promote.spec.ts @@ -134,7 +134,7 @@ async function promoteSoloToAdmin(platform: SupportedPlatformsType, testInfo: Te await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await setDisappearingMessage(bob1, ['Group', timerType, time]); await Promise.all( [alice1, charlie1].map(device => device.waitForControlMessageToBePresent( @@ -224,7 +224,7 @@ async function promoteSoloLinked(platform: SupportedPlatformsType, testInfo: Tes await device1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, device2, ['Group', timerType, time]); + await setDisappearingMessage(device2, ['Group', timerType, time]); await Promise.all( [device1, device3].map(device => device.waitForControlMessageToBePresent( @@ -331,7 +331,7 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T await alice1.navigateBack(); await test.step(`Verify ${bob.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages - await setDisappearingMessage(platform, bob1, ['Group', timerType, time]); + await setDisappearingMessage(bob1, ['Group', timerType, time]); await Promise.all( [alice1, charlie1].map(device => device.waitForControlMessageToBePresent( @@ -351,7 +351,7 @@ async function promoteMultiToAdmin(platform: SupportedPlatformsType, testInfo: T await test.step(`Verify ${charlie.userName} has admin powers by setting disappearing messages`, async () => { // Check to see if Bob has admin powers by setting disappearing messages const charlieTime = DISAPPEARING_TIMES.TWELVE_HOURS; - await setDisappearingMessage(platform, charlie1, ['Group', timerType, charlieTime]); + await setDisappearingMessage(charlie1, ['Group', timerType, charlieTime]); await Promise.all( [alice1, bob1].map(device => device.waitForControlMessageToBePresent( diff --git a/run/test/utils/create_group.ts b/run/test/utils/create_group.ts index cbf49f249..55820a0aa 100644 --- a/run/test/utils/create_group.ts +++ b/run/test/utils/create_group.ts @@ -9,7 +9,6 @@ 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, @@ -48,7 +47,6 @@ export const createGroup = async ( await device1.clickOnElementAll({ ...new Contact(device1).build(), text: userThree.userName }); // Select tick await device1.clickOnElementAll(new CreateGroupButton(device1)); - await sleepFor(3000); // Enter group chat on device 2 and 3 await Promise.all([ device2.onAndroid().navigateBack(false), @@ -76,26 +74,17 @@ export const createGroup = async ( ), ]); } - // Send message from User A to group to verify all working - await device1.sendMessage(aliceMessage); - // Did the other devices receive alice's message? - await Promise.all( - [device2, device3].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, aliceMessage)) - ) - ); - // Send message from User B to group - await device2.sendMessage(bobMessage); - await Promise.all( - [device1, device3].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, bobMessage)) - ) - ); - // Send message to User C to group - await device3.sendMessage(charlieMessage); + // Send messages from all three users simultaneously to verify group is working + await Promise.all([ + device1.sendMessage(aliceMessage), + device2.sendMessage(bobMessage), + device3.sendMessage(charlieMessage), + ]); + // Verify all messages are visible on all devices + const allMessages = [aliceMessage, bobMessage, charlieMessage]; await Promise.all( - [device1, device2].map(device => - device.waitForTextElementToBePresent(new MessageBody(device, charlieMessage)) + [device1, device2, device3].flatMap(device => + allMessages.map(message => device.waitForTextElementToBePresent(new MessageBody(device, message))) ) ); return { userName, userOne, userTwo, userThree }; diff --git a/run/test/utils/restore_account.ts b/run/test/utils/restore_account.ts index ae93f62d8..26b7c9592 100644 --- a/run/test/utils/restore_account.ts +++ b/run/test/utils/restore_account.ts @@ -82,7 +82,6 @@ export const restoreAccountNoFallback = async ( // Wait for permissions modal to pop up await sleepFor(500); await handleNotificationPermissions(device, allowNotificationPermissions); - await sleepFor(1000); // Check that we're on the home screen await device.waitForTextElementToBePresent(new PlusButton(device)); }; diff --git a/run/test/utils/set_disappearing_messages.ts b/run/test/utils/set_disappearing_messages.ts index b35ad1b25..29683962a 100644 --- a/run/test/utils/set_disappearing_messages.ts +++ b/run/test/utils/set_disappearing_messages.ts @@ -6,52 +6,21 @@ import { DisappearingMessagesMenuOption, DisappearingMessagesSubtitle, DisappearingMessagesTimerType, - FollowSettingsButton, SetDisappearMessagesButton, - SetModalButton, } from '../locators/disappearing_messages'; -import { SupportedPlatformsType } from './open_app'; -import { sleepFor } from './sleep_for'; export const setDisappearingMessage = async ( - platform: SupportedPlatformsType, device: DeviceWrapper, - [conversationType, timerType, timerDuration = DISAPPEARING_TIMES.THIRTY_SECONDS]: MergedOptions, - device2?: DeviceWrapper + [conversationType, timerType, timerDuration = DISAPPEARING_TIMES.THIRTY_SECONDS]: MergedOptions ) => { const enforcedType: ConversationType = conversationType; await device.clickOnElementAll(new ConversationSettings(device)); - // Wait for UI to load conversation options menu - await sleepFor(500); await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); if (enforcedType === '1:1') { await device.clickOnElementAll(new DisappearingMessagesTimerType(device, timerType)); } - if (timerType === 'Disappear after read option') { - if (enforcedType === '1:1') { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.TWELVE_HOURS); - } else { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); - } - } else if ( - enforcedType === 'Group' || - (enforcedType === 'Note to Self' && timerType === 'Disappear after send option') - ) { - await device.onIOS().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_IOS); - await device.onAndroid().disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.OFF_ANDROID); - } else { - await device.disappearRadioButtonSelected(platform, DISAPPEARING_TIMES.ONE_DAY); - } - await device.clickOnElementAll(new DisappearingMessageRadial(device, timerDuration)); await device.clickOnElementAll(new SetDisappearMessagesButton(device)); 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) { - await device2.clickOnElementAll(new FollowSettingsButton(device2)); - await sleepFor(500); - await device2.clickOnElementAll(new SetModalButton(device2)); - } await device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)); }; From 427fdadc309b2b99fb52ba7c65b49804db324f36 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 17:08:16 +1100 Subject: [PATCH 116/184] refactor: parallelize matchAndTapImage --- run/test/utils/click_by_coordinates.ts | 2 - run/test/utils/create_group.ts | 4 +- run/types/DeviceWrapper.ts | 168 +++++++++++-------------- 3 files changed, 78 insertions(+), 96 deletions(-) diff --git a/run/test/utils/click_by_coordinates.ts b/run/test/utils/click_by_coordinates.ts index 707649b88..83320ff97 100644 --- a/run/test/utils/click_by_coordinates.ts +++ b/run/test/utils/click_by_coordinates.ts @@ -1,10 +1,8 @@ import { DeviceWrapper } from '../../types/DeviceWrapper'; import { Coordinates } from '../../types/testing'; -import { sleepFor } from './sleep_for'; export const clickOnCoordinates = async (device: DeviceWrapper, coordinates: Coordinates) => { const { x, y } = coordinates; - await sleepFor(1000); await device.pressCoordinates(x, y); device.log(`Tapped coordinates ${x}, ${y}`); }; diff --git a/run/test/utils/create_group.ts b/run/test/utils/create_group.ts index 55820a0aa..a5e1bc491 100644 --- a/run/test/utils/create_group.ts +++ b/run/test/utils/create_group.ts @@ -84,7 +84,9 @@ export const createGroup = async ( const allMessages = [aliceMessage, bobMessage, charlieMessage]; await Promise.all( [device1, device2, device3].flatMap(device => - allMessages.map(message => device.waitForTextElementToBePresent(new MessageBody(device, message))) + allMessages.map(message => + device.waitForTextElementToBePresent(new MessageBody(device, message)) + ) ) ); return { userName, userOne, userTwo, userThree }; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 0eb378eac..d72de0c5d 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1096,120 +1096,105 @@ export class DeviceWrapper { */ public async matchAndTapImage( locator: StrategyExtractionObj, - referenceImageName: string, - earlyMatch: boolean = true + referenceImageName: string ): Promise { const threshold = 0.85; - const earlyMatchThreshold = 0.97; - // Find all candidate elements matching the locator - const elements = await this.findElements(locator.strategy, locator.selector); + // Retry findElements with exponential backoff — photo picker may not have rendered yet + let elements = await this.findElements(locator.strategy, locator.selector); + if (elements.length === 0) { + let delay = 100; + const maxWait = 5000; + const start = Date.now(); + while (elements.length === 0 && Date.now() - start < maxWait) { + await sleepFor(delay); + delay = Math.min(delay * 2, 1600); + elements = await this.findElements(locator.strategy, locator.selector); + } + } + this.info( `[matchAndTapImage] Starting image matching: ${elements.length} elements with ${locator.strategy} "${locator.selector}"` ); - // Load the reference image buffer from disk + // Load the reference image buffer from disk once const referencePath = path.join('run', 'test', 'media', referenceImageName); await fs.access(referencePath).catch(() => { throw new Error(`Reference image not found: ${referencePath}`); }); const referenceBuffer = await fs.readFile(referencePath); + // Hoist reference metadata — it never changes across elements + const refMeta = await sharp(referenceBuffer).metadata(); - let bestMatch: { - center: { x: number; y: number }; - score: number; - } | null = null; - - // Iterate over each candidate element - for (const el of elements) { - // Take a screenshot of the element - const base64 = await this.getElementScreenshot(el.ELEMENT); - const elementBuffer = Buffer.from(base64, 'base64'); + // Phase 1: screenshot + comparison in parallel — no rect yet + const results = await Promise.all( + elements.map(async el => { + const base64 = await this.getElementScreenshot(el.ELEMENT); + const elementBuffer = Buffer.from(base64, 'base64'); - // Get the element's rectangle (position and size) - const rect = await this.getElementRect(el.ELEMENT); - if (!rect) { - continue; - } - // Get actual pixel dimensions of the element screenshot - const elementMeta = await sharp(elementBuffer).metadata(); - // Get original reference image dimensions - const refMeta = await sharp(referenceBuffer).metadata(); + const elementMeta = await sharp(elementBuffer).metadata(); - let resizedRef: Buffer; + let resizedRef: Buffer; + let resizedMeta: Awaited>; - if (elementMeta.width === refMeta.width && elementMeta.height === refMeta.height) { - // Skip resizing if reference already matches the screenshot dimensions - resizedRef = referenceBuffer; - } else { - // Resize the reference image to exactly match the screenshot dimensions - const targetWidth = elementMeta.width; - const targetHeight = elementMeta.height; + if (elementMeta.width === refMeta.width && elementMeta.height === refMeta.height) { + // Skip resizing if reference already matches the screenshot dimensions + resizedRef = referenceBuffer; + resizedMeta = refMeta; + } else { + resizedRef = await sharp(referenceBuffer) + .resize(elementMeta.width, elementMeta.height) + .toBuffer(); + resizedMeta = await sharp(resizedRef).metadata(); + } - resizedRef = await sharp(referenceBuffer).resize(targetWidth, targetHeight).toBuffer(); - } + try { + const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { + threshold, + }); + return { el, matchRect, score, resizedMeta }; + } catch { + return null; + } + }) + ); - try { - const { rect: matchRect, score } = await getImageOccurrence(elementBuffer, resizedRef, { - threshold, - }); + type MatchResult = NonNullable<(typeof results)[number]>; + const bestResult = results + .filter((r): r is MatchResult => r !== null) + .reduce((best, r) => (!best || r.score > best.score ? r : best), null); - /** - * 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) { - this.info( - `[matchAndTapImage] Tapping first high-confidence match (${(score * 100).toFixed(2)}%)` - ); - await clickOnCoordinates(this, center); - return; - } - // Otherwise, keep track of the best match so far - if (!bestMatch || score > bestMatch.score) { - bestMatch = { center, score }; - } - } catch { - continue; // No match in this element, try next - } - } - // If no good match was found, throw an error - if (!bestMatch) { + if (!bestResult) { console.log( `[matchAndTapImage] No matching image found among ${elements.length} elements for ${locator.strategy} "${locator.selector}"` ); throw new Error('Unable to find the expected UI element on screen'); } - // Tap the best match found - this.info( - `[matchAndTapImage] Tapping best match with ${(bestMatch.score * 100).toFixed(2)}% confidence` - ); - await clickOnCoordinates(this, bestMatch.center); + + // Phase 2: fetch rect only for the winning element + const rect = await this.getElementRect(bestResult.el.ELEMENT); + + if (!rect) { + throw new Error('Unable to get rect for matched element'); + } + + /** + * 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. + */ + const { matchRect, resizedMeta } = bestResult; + const scaleX = rect.width / (resizedMeta.width ?? rect.width); + const scaleY = rect.height / (resizedMeta.height ?? rect.height); + const matchCenterX = matchRect.x + Math.floor(matchRect.width / 2); + const matchCenterY = matchRect.y + Math.floor(matchRect.height / 2); + const tapX = Math.round(rect.x + matchCenterX * scaleX); + const tapY = Math.round(rect.y + matchCenterY * scaleY); + + await clickOnCoordinates(this, { x: tapX, y: tapY }); } /** * Checks if an element exists on the screen without throwing an error. @@ -1961,7 +1946,6 @@ export class DeviceWrapper { await this.clickOnElementAll(new ImagesFolderButton(this)); await sleepFor(1000); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); - await sleepFor(2000); // Appium needs a moment, matchAndTapImage sometimes finds 0 elements otherwise await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, testImage @@ -2008,7 +1992,6 @@ export class DeviceWrapper { strategy: 'accessibility id', selector: 'Allow Full Access', }); - 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` }, @@ -2227,7 +2210,6 @@ export class DeviceWrapper { // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { 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` }, uploadPicture From 5cd28de18270f205ae39cbffd50a68be0178529f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 23 Feb 2026 17:12:03 +1100 Subject: [PATCH 117/184] chore: update comment --- run/types/DeviceWrapper.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index d72de0c5d..73c7fa47c 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1083,15 +1083,14 @@ export class DeviceWrapper { const message = await this.findMatchingTextAndAccessibilityId('Message body', textToLookFor); return message; } + /** - * Attempts to visually match a reference image against all elements found by the given locator, - * and taps the best match (or the first high-confidence match if earlyMatch is enabled). - * This is useful for scenarios where UI elements cannot be reliably identified, - * such as elements with date-based accessibility IDs. + * Attempts to visually match a reference image against all instances found by the given locator, and taps the best match. + * All element screenshots are taken in parallel. + * If the method finds 0 results for a locator, retries with exponential backoff up to 5 seconds. * * @param locator - The strategy and selector to find candidate elements. * @param referenceImageName - The filename of the reference image (in the media directory). - * @param earlyMatch - If true, taps immediately on the first match above the earlyMatchThreshold. * @throws If no suitable match is found among the candidate elements. */ public async matchAndTapImage( @@ -1123,10 +1122,10 @@ export class DeviceWrapper { throw new Error(`Reference image not found: ${referencePath}`); }); const referenceBuffer = await fs.readFile(referencePath); - // Hoist reference metadata — it never changes across elements + // Reference metadata never changes across elements const refMeta = await sharp(referenceBuffer).metadata(); - // Phase 1: screenshot + comparison in parallel — no rect yet + // Phase 1: screenshot + comparison in parallel const results = await Promise.all( elements.map(async el => { const base64 = await this.getElementScreenshot(el.ELEMENT); @@ -1171,7 +1170,7 @@ export class DeviceWrapper { throw new Error('Unable to find the expected UI element on screen'); } - // Phase 2: fetch rect only for the winning element + // Phase 2: fetch rect only for the winning element to determine tap coords const rect = await this.getElementRect(bestResult.el.ELEMENT); if (!rect) { @@ -1196,6 +1195,7 @@ export class DeviceWrapper { await clickOnCoordinates(this, { x: tapX, y: tapY }); } + /** * Checks if an element exists on the screen without throwing an error. * Only useful for scenarios where you want to interact with an element if it exists From 4f14521b231b1d1fb59b1e0f60c9905da48f41d6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Feb 2026 10:04:43 +1100 Subject: [PATCH 118/184] chore: trim logs more aggressively --- run/test/utils/failure_artifacts.ts | 2 +- run/types/DeviceWrapper.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/run/test/utils/failure_artifacts.ts b/run/test/utils/failure_artifacts.ts index 3fd772322..9f82121c3 100644 --- a/run/test/utils/failure_artifacts.ts +++ b/run/test/utils/failure_artifacts.ts @@ -240,7 +240,7 @@ async function collectLogBuffer( return null; } -const MAX_LOG_BYTES = 2 * 1024 * 1024; // 2 MB — tail beyond this to keep reports lean +const MAX_LOG_BYTES = 1024 * 1024; // 1 MB — tail beyond this to keep reports lean function tailBuffer(raw: Buffer): Buffer { if (raw.length <= MAX_LOG_BYTES) return raw; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 73c7fa47c..550fb419b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1918,6 +1918,9 @@ export class DeviceWrapper { }); if (this.isIOS()) { // Push file to simulator + this.warn( + `pushMediaToDevice on iOS is deprecated. Consider pre-loading it on simulator creation` + ); await runScriptAndLog(`xcrun simctl addmedia ${this.udid} ${filePath}`, true); } else if (this.isAndroid()) { const ANDROID_DOWNLOAD_DIR = '/storage/emulated/0/Download'; From b0ffb012149395c8cfe247e6109717688e0e6d17 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Feb 2026 10:18:40 +1100 Subject: [PATCH 119/184] fix: wait for empty state to disappear when joining community --- run/test/utils/community.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index 9001f333b..d547bf27d 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -16,7 +16,7 @@ export const joinCommunity = async ( await device.inputText(communityLink, new CommunityInput(device)); await device.clickOnElementAll(new JoinCommunityButton(device)); await device.waitForTextElementToBePresent(new ConversationHeaderName(device, communityName)); - await device.verifyElementNotPresent(new EmptyConversation(device)); // checking that messages loaded already + await device.hasElementBeenDeleted(new EmptyConversation(device)); // checking that messages loaded already await device.scrollToBottom(); }; From 7fe749f03df42f975950b7293a98bb983e157c75 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Feb 2026 11:03:18 +1100 Subject: [PATCH 120/184] refactor: get rid of disappearing control messages util --- run/test/specs/disappear_after_read.spec.ts | 25 ++++---- run/test/specs/disappear_after_send.spec.ts | 27 +++++---- .../disappear_after_send_off_1o1.spec.ts | 27 +++++---- .../utils/disappearing_control_messages.ts | 60 ------------------- 4 files changed, 46 insertions(+), 93 deletions(-) delete mode 100644 run/test/utils/disappearing_control_messages.ts diff --git a/run/test/specs/disappear_after_read.spec.ts b/run/test/specs/disappear_after_read.spec.ts index d80ac5507..68964e545 100644 --- a/run/test/specs/disappear_after_read.spec.ts +++ b/run/test/specs/disappear_after_read.spec.ts @@ -1,11 +1,11 @@ import { test, type TestInfo } from '@playwright/test'; +import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; import { MessageBody } from '../locators/conversation'; import { open_Alice1_Bob1_friends } from '../state_builder'; -import { checkDisappearingControlMessage } from '../utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; import { setDisappearingMessage } from '../utils/set_disappearing_messages'; @@ -43,17 +43,20 @@ async function disappearAfterRead(platform: SupportedPlatformsType, testInfo: Te await test.step(TestSteps.DISAPPEARING_MESSAGES.SET(time), async () => { await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); }); - // Check control message is correct on device 2 + // Check control messages on both devices await test.step(TestSteps.VERIFY.DISAPPEARING_CONTROL_MESSAGES, async () => { - await checkDisappearingControlMessage( - platform, - alice.userName, - bob.userName, - alice1, - bob1, - time, - mode - ); + await Promise.all([ + alice1.waitForControlMessageToBePresent( + tStripped('disappearingMessagesSetYou', { time, disappearing_messages_type: mode }) + ), + bob1.waitForControlMessageToBePresent( + tStripped('disappearingMessagesSet', { + name: alice.userName, + time, + disappearing_messages_type: mode, + }) + ), + ]); }); // Send message to verify that deletion is working await test.step(TestSteps.SEND.MESSAGE(alice.userName, bob.userName), async () => { diff --git a/run/test/specs/disappear_after_send.spec.ts b/run/test/specs/disappear_after_send.spec.ts index 780122c56..7e5638c4a 100644 --- a/run/test/specs/disappear_after_send.spec.ts +++ b/run/test/specs/disappear_after_send.spec.ts @@ -1,10 +1,10 @@ import type { TestInfo } from '@playwright/test'; +import { tStripped } from '../../localizer/lib'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DisappearActions, DISAPPEARING_TIMES, DisappearModes } from '../../types/testing'; import { MessageBody } from '../locators/conversation'; import { open_Alice1_Bob1_friends } from '../state_builder'; -import { checkDisappearingControlMessage } from '../utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; import { setDisappearingMessage } from '../utils/set_disappearing_messages'; @@ -23,7 +23,7 @@ bothPlatformsIt({ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, - prebuilt: { alice, bob }, + prebuilt: { alice }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, @@ -36,16 +36,19 @@ async function disappearAfterSend(platform: SupportedPlatformsType, testInfo: Te const maxWait = 35_000; // 30s plus buffer // Select disappearing messages option await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); - // Get control message based on key from json file - await checkDisappearingControlMessage( - platform, - alice.userName, - bob.userName, - alice1, - bob1, - time, - controlMode - ); + // Check control messages on both devices + await Promise.all([ + alice1.waitForControlMessageToBePresent( + tStripped('disappearingMessagesSetYou', { time, disappearing_messages_type: controlMode }) + ), + bob1.waitForControlMessageToBePresent( + tStripped('disappearingMessagesSet', { + name: alice.userName, + time, + disappearing_messages_type: controlMode, + }) + ), + ]); // Send message to verify that deletion is working const sentTimestamp = await alice1.sendMessage(testMessage); // Wait for message to disappear 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 998d9688c..ac1dd9e5a 100644 --- a/run/test/specs/disappear_after_send_off_1o1.spec.ts +++ b/run/test/specs/disappear_after_send_off_1o1.spec.ts @@ -11,8 +11,8 @@ import { FollowSettingsButton, SetDisappearMessagesButton, } from '../locators/disappearing_messages'; +import { ConversationItem } from '../locators/home'; import { open_Alice2_Bob1_friends } from '../state_builder'; -import { checkDisappearingControlMessage } from '../utils/disappearing_control_messages'; import { closeApp, SupportedPlatformsType } from '../utils/open_app'; import { setDisappearingMessage } from '../utils/set_disappearing_messages'; @@ -40,17 +40,24 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn const time = DISAPPEARING_TIMES.THIRTY_SECONDS; // Select disappearing messages option await setDisappearingMessage(alice1, ['1:1', `Disappear after ${mode} option`, time]); - // Get control message based on key from json file - await checkDisappearingControlMessage( - platform, - alice.userName, - bob.userName, - alice1, - bob1, + // Check control messages on both devices and sync to linked device + const setYouMsg = tStripped('disappearingMessagesSetYou', { time, - controlMode, + disappearing_messages_type: controlMode, + }); + await Promise.all([ + alice1.waitForControlMessageToBePresent(setYouMsg), + bob1.waitForControlMessageToBePresent( + tStripped('disappearingMessagesSet', { + name: alice.userName, + time, + disappearing_messages_type: controlMode, + }) + ), alice2 - ); + .clickOnElementAll(new ConversationItem(alice2, bob.userName)) + .then(() => alice2.waitForControlMessageToBePresent(setYouMsg)), + ]); // Turn off disappearing messages on device 1 await alice1.clickOnElementAll(new ConversationSettings(alice1)); diff --git a/run/test/utils/disappearing_control_messages.ts b/run/test/utils/disappearing_control_messages.ts deleted file mode 100644 index db29c2702..000000000 --- a/run/test/utils/disappearing_control_messages.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { UserNameType } from '@session-foundation/qa-seeder'; - -import { tStripped } from '../../localizer/lib'; -import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { DisappearActions, DISAPPEARING_TIMES } from '../../types/testing'; -import { ConversationItem } from '../locators/home'; -import { SupportedPlatformsType } from './open_app'; - -export const checkDisappearingControlMessage = async ( - platform: SupportedPlatformsType, - userNameA: UserNameType, - userNameB: UserNameType, - device1: DeviceWrapper, - device2: DeviceWrapper, - time: DISAPPEARING_TIMES, - mode: DisappearActions, - linkedDevice?: DeviceWrapper -) => { - // Two control messages to check - You have set and other user has set - // "disappearingMessagesSet": "{name} has set messages to disappear {time} after they have been {disappearing_messages_type}.", - const disappearingMessagesSetAlice = tStripped('disappearingMessagesSet', { - name: userNameA, - time, - disappearing_messages_type: mode, - }); - const disappearingMessagesSetBob = tStripped('disappearingMessagesSet', { - name: userNameB, - time, - disappearing_messages_type: mode, - }); - // "disappearingMessagesSetYou": "You set messages to disappear {time} after they have been {disappearing_messages_type}.", - const disappearingMessagesSetYou = tStripped('disappearingMessagesSetYou', { - time, - disappearing_messages_type: mode, - }); - // Check device 1 - if (platform === 'android') { - await Promise.all([ - device1.waitForControlMessageToBePresent(disappearingMessagesSetYou), - device1.waitForControlMessageToBePresent(disappearingMessagesSetBob), - ]); - // Check device 2 - await Promise.all([ - device2.waitForControlMessageToBePresent(disappearingMessagesSetYou), - device2.waitForControlMessageToBePresent(disappearingMessagesSetAlice), - ]); - } - if (platform === 'ios') { - await Promise.all([ - device1.waitForControlMessageToBePresent(disappearingMessagesSetYou), - device2.waitForControlMessageToBePresent(disappearingMessagesSetAlice), - ]); - } - // Check if control messages are syncing from both user A and user B - if (linkedDevice) { - await linkedDevice.clickOnElementAll(new ConversationItem(linkedDevice, userNameB)); - await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetYou); - await linkedDevice.waitForControlMessageToBePresent(disappearingMessagesSetBob); - } -}; From 12d795f35c21e5757efb249708394908ba92bb45 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Feb 2026 13:23:17 +1100 Subject: [PATCH 121/184] fix: more Android 1.32.0 fixes --- .../disappear_after_send_off_1o1.spec.ts | 8 ------- ...appearing_messages_follow_settings.spec.ts | 21 ++++++++++++++++--- run/test/specs/message_voice.spec.ts | 2 +- run/test/utils/community.ts | 4 ++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/run/test/specs/disappear_after_send_off_1o1.spec.ts b/run/test/specs/disappear_after_send_off_1o1.spec.ts index ac1dd9e5a..b9c1eafe4 100644 --- a/run/test/specs/disappear_after_send_off_1o1.spec.ts +++ b/run/test/specs/disappear_after_send_off_1o1.spec.ts @@ -8,7 +8,6 @@ import { DisableDisappearingMessages, DisappearingMessagesMenuOption, DisappearingMessagesSubtitle, - FollowSettingsButton, SetDisappearMessagesButton, } from '../locators/disappearing_messages'; import { ConversationItem } from '../locators/home'; @@ -77,13 +76,6 @@ async function disappearAfterSendOff1o1(platform: SupportedPlatformsType, testIn bob1.waitForControlMessageToBePresent(disappearingMessagesTurnedOff), alice2.waitForControlMessageToBePresent(disappearingMessagesTurnedOffYou), ]); - // Follow setting on device 2 - await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); - await bob1.checkModalStrings( - tStripped('disappearingMessagesFollowSetting'), - tStripped('disappearingMessagesFollowSettingOff') - ); - await bob1.clickOnElementAll({ strategy: 'accessibility id', selector: 'Confirm' }); // Check conversation subtitle? await Promise.all( [alice1, bob1, alice2].map(device => diff --git a/run/test/specs/disappearing_messages_follow_settings.spec.ts b/run/test/specs/disappearing_messages_follow_settings.spec.ts index 49955f013..4b8a68b87 100644 --- a/run/test/specs/disappearing_messages_follow_settings.spec.ts +++ b/run/test/specs/disappearing_messages_follow_settings.spec.ts @@ -3,6 +3,7 @@ import type { TestInfo } from '@playwright/test'; import { tStripped } from '../../localizer/lib'; import { bothPlatformsIt } from '../../types/sessionIt'; import { DISAPPEARING_TIMES } from '../../types/testing'; +import { MessageBody } from '../locators/conversation'; import { DisappearingMessagesSubtitle, FollowSettingsButton, @@ -34,10 +35,11 @@ async function disappearingMessagesFollowSetting1o1( ) { const { devices: { alice1, bob1 }, + prebuilt: { alice, bob }, } = await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: true, testInfo }); - + const aliceMsg = `${alice.userName}'s messages will disappear`; + const bobMsg = `${bob.userName}'s messages will disappear`; await setDisappearingMessage(alice1, ['1:1', timerType, time]); - // Bob should see the follow settings banner after Alice sets DM await bob1.clickOnElementAll(new FollowSettingsButton(bob1)); await bob1.checkModalStrings( @@ -55,6 +57,19 @@ async function disappearingMessagesFollowSetting1o1( device.waitForTextElementToBePresent(new DisappearingMessagesSubtitle(device)) ) ); - + const aliceTimestamp = await alice1.sendMessage(aliceMsg); + const bobTimestamp = await bob1.sendMessage(bobMsg); + await Promise.all( + [alice1, bob1].flatMap(device => [ + device.hasElementDisappeared({ + ...new MessageBody(device, aliceMsg).build(), + actualStartTime: aliceTimestamp, + }), + device.hasElementDisappeared({ + ...new MessageBody(device, bobMsg).build(), + actualStartTime: bobTimestamp, + }), + ]) + ); await closeApp(alice1, bob1); } diff --git a/run/test/specs/message_voice.spec.ts b/run/test/specs/message_voice.spec.ts index 68164a123..1e155aefe 100644 --- a/run/test/specs/message_voice.spec.ts +++ b/run/test/specs/message_voice.spec.ts @@ -31,7 +31,7 @@ async function sendVoiceMessage(platform: SupportedPlatformsType, testInfo: Test await sleepFor(500); // The voice message long tap must be offset so that it doesn't tap the scrubber // As this starts playback and does not open the long press menu - await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 100 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index d547bf27d..4645e52e4 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -2,7 +2,7 @@ import test from '@playwright/test'; import { DeviceWrapper } from '../../types/DeviceWrapper'; import { CommunityInput, JoinCommunityButton } from '../locators'; -import { ConversationHeaderName, EmptyConversation } from '../locators/conversation'; +import { ConversationHeaderName, MessageBody } from '../locators/conversation'; import { PlusButton } from '../locators/home'; import { JoinCommunityOption } from '../locators/start_conversation'; @@ -16,7 +16,7 @@ export const joinCommunity = async ( await device.inputText(communityLink, new CommunityInput(device)); await device.clickOnElementAll(new JoinCommunityButton(device)); await device.waitForTextElementToBePresent(new ConversationHeaderName(device, communityName)); - await device.hasElementBeenDeleted(new EmptyConversation(device)); // checking that messages loaded already + await device.waitForTextElementToBePresent(new MessageBody(device)); // Check for ANY message await device.scrollToBottom(); }; From b3ce61af858c009194ed6b2614091eceba6bc25c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Feb 2026 14:58:35 +1100 Subject: [PATCH 122/184] fix: also offset group voice msg more --- run/test/specs/group_message_voice.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index 3cd346c33..d7e6f49a3 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -42,7 +42,7 @@ async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: ); // The voice message long tap must be offset so that it doesn't tap the scrubber // As this starts playback and does not open the long press menu - await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 50 } }); + await bob1.longPressMessage(new VoiceMessage(bob1), { offset: { x: 0, y: 100 } }); await bob1.clickOnByAccessibilityID('Reply to message'); await sleepFor(500); // Let the UI settle before finding message input and typing await bob1.sendMessage(replyMessage); From 28eae1cb782b862748c7e740f00bbdc5c6c37664 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Feb 2026 13:38:58 +1100 Subject: [PATCH 123/184] feat: pin conversation test --- run/test/locators/home.ts | 51 ++++++++++++ run/test/specs/group_message_voice.spec.ts | 2 +- run/test/specs/user_actions_pin_unpin.spec.ts | 82 +++++++++++++++++++ run/test/utils/conversation_order.ts | 28 +++++++ run/types/DeviceWrapper.ts | 26 ++++-- run/types/allure.ts | 1 + run/types/testing.ts | 3 + 7 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 run/test/specs/user_actions_pin_unpin.spec.ts create mode 100644 run/test/utils/conversation_order.ts diff --git a/run/test/locators/home.ts b/run/test/locators/home.ts index 82f03f9eb..d9ba6d6c8 100644 --- a/run/test/locators/home.ts +++ b/run/test/locators/home.ts @@ -50,6 +50,27 @@ export class ConversationItem extends LocatorsInterface { } } +// Find pin icon belonging to a specific conversation +export class ConversationPinnedIcon extends LocatorsInterface { + constructor( + device: DeviceWrapper, + private name: string + ) { + super(device); + } + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'xpath', + selector: `//android.view.ViewGroup[android.widget.TextView[@content-desc='Conversation list item' and @text='${this.name}']]/android.widget.ImageView[@resource-id='network.loki.messenger:id/iconPinned']`, + } as const; + case 'ios': + throw new Error('ConversationPinnedIcon: iOS not yet implemented'); + } + } +} + export class EmptyLandingPage extends LocatorsInterface { public build() { switch (this.platform) { @@ -140,6 +161,19 @@ export class MessageSnippet extends LocatorsInterface { } } } +export class PinConversationOption extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Pin', + } as const; + } + } +} + export class PlusButton extends LocatorsInterface { public build() { return { @@ -250,3 +284,20 @@ export class SearchButton extends LocatorsInterface { } } } + +export class UnpinConversationOption extends LocatorsInterface { + public build(): StrategyExtractionObj { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'network.loki.messenger:id/unpinTextView', + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'Unpin', + } as const; + } + } +} diff --git a/run/test/specs/group_message_voice.spec.ts b/run/test/specs/group_message_voice.spec.ts index d7e6f49a3..313bcf939 100644 --- a/run/test/specs/group_message_voice.spec.ts +++ b/run/test/specs/group_message_voice.spec.ts @@ -16,7 +16,7 @@ bothPlatformsIt({ suite: 'Message types', }, allureDescription: - 'Verifies that a voice message can be sent to a group, all members receive the document, and replying to a document works as expected', + 'Verifies that a voice message can be sent to a group, all members receive it, and replying to it works as expected', }); async function sendVoiceMessageGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_pin_unpin.spec.ts b/run/test/specs/user_actions_pin_unpin.spec.ts new file mode 100644 index 000000000..a36425546 --- /dev/null +++ b/run/test/specs/user_actions_pin_unpin.spec.ts @@ -0,0 +1,82 @@ +import { test, type TestInfo } from '@playwright/test'; +import { USERNAME } from '@session-foundation/qa-seeder'; + +import { communities } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { bothPlatformsIt } from '../../types/sessionIt'; +import { ConversationPinnedIcon } from '../locators/home'; +import { joinCommunity } from '../utils/community'; +import { assertPinOrder, getConversationOrder } from '../utils/conversation_order'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +bothPlatformsIt({ + title: 'Pin and unpin conversation', + risk: 'medium', + testCb: pinConversationTest, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'User Actions', + suite: 'Pin/Unpin', + }, + allureDescription: + 'Verifies that pinning moves a conversation to the top of the list and unpinning restores the original order', +}); + +async function pinConversationTest(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + + await test.step('Join two communities', async () => { + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); + await device.navigateBack(); + await joinCommunity(device, communities.lokinetUpdates.link, communities.lokinetUpdates.name); + await device.navigateBack(); + }); + + let beforeOrder: string[] = []; + let toPin = ''; + + await test.step('Capture conversation order before pinning', async () => { + beforeOrder = await getConversationOrder(device); + toPin = beforeOrder[beforeOrder.length - 1]; + device.log(`Pinning last conversation: "${toPin}"`); + }); + + await test.step(`Pin "${toPin}"`, async () => { + await device.pinConversation(toPin); + }); + + await test.step('Assert pinned conversation moved to top', async () => { + const afterOrder = await getConversationOrder(device); + assertPinOrder(beforeOrder, [toPin], afterOrder); + }); + + if (platform === 'android') { + await test.step('Assert pin icon is visible on pinned conversation', async () => { + await device.waitForTextElementToBePresent(new ConversationPinnedIcon(device, toPin)); + }); + } + + await test.step(`Unpin "${toPin}"`, async () => { + await device.unpinConversation(toPin); + }); + + await test.step('Assert order restored after unpinning', async () => { + const afterUnpinOrder = await getConversationOrder(device); + assertPinOrder(beforeOrder, [], afterUnpinOrder); + }); + + if (platform === 'android') { + await test.step('Assert pin icon is gone after unpinning', async () => { + await device.verifyElementNotPresent(new ConversationPinnedIcon(device, toPin)); + }); + } + + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts new file mode 100644 index 000000000..45131bd27 --- /dev/null +++ b/run/test/utils/conversation_order.ts @@ -0,0 +1,28 @@ +import { DeviceWrapper } from '../../types/DeviceWrapper'; + +// Returns the names of all conversation list items in their current DOM order +export const getConversationOrder = async (device: DeviceWrapper): Promise => { + const items = await device.findElementsByAccessibilityId('Conversation list item'); + return Promise.all(items.map(item => device.getTextFromElement(item))); +}; + +// Asserts pinned conversations float to the top maintaining relative order, followed by unpinned in their original order. +// Pass an empty pinnedNames array to assert the order is fully restored (e.g. after unpinning). +export const assertPinOrder = ( + beforeOrder: string[], + pinnedNames: string[], + afterOrder: string[] +) => { + const expected = [ + ...beforeOrder.filter(n => pinnedNames.includes(n)), + ...beforeOrder.filter(n => !pinnedNames.includes(n)), + ]; + + for (let i = 0; i < expected.length; i++) { + if (afterOrder[i] !== expected[i]) { + console.log(`Conversation order wrong at position ${i + 1}: expected "${expected[i]}" but got "${afterOrder[i]}". + Full order: [${afterOrder.join(', ')}]`); + throw new Error(`Conversations are not in the correct order after (un)pinning`); + } + } +}; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 550fb419b..1b16ddcff 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -60,7 +60,9 @@ import { ConversationItem, MessageRequestItem, MessageRequestsBanner, + PinConversationOption, PlusButton, + UnpinConversationOption, } from '../test/locators/home'; import { LoadingAnimation } from '../test/locators/onboarding'; import { @@ -845,12 +847,12 @@ export class DeviceWrapper { await this.longClick(el, 3000); await sleepFor(1000); - // Pin is the only consistent option in context menu - const longPressSuccess = await this.waitForTextElementToBePresent({ - strategy: 'accessibility id', - selector: 'Pin', - maxWait: 1000, - }); + // Either Pin or Unpin will be present depending on whether the conversation is already pinned + const longPressSuccess = await this.findWithFallback( + new PinConversationOption(this), + new UnpinConversationOption(this), + 1000 + ); if (longPressSuccess) { this.log('LongClick successful'); @@ -872,6 +874,18 @@ export class DeviceWrapper { } } + public async pinConversation(name: string) { + await this.onIOS().swipeLeft('Conversation list item', name); + await this.onAndroid().longPressConversation(name); + await this.clickOnElementAll(new PinConversationOption(this)); + } + + public async unpinConversation(name: string) { + await this.onIOS().swipeLeft('Conversation list item', name); + await this.onAndroid().longPressConversation(name); + await this.clickOnElementAll(new UnpinConversationOption(this)); + } + public async pressAndHold(accessibilityId: AccessibilityId) { const el = await this.waitForTextElementToBePresent({ strategy: 'accessibility id', diff --git a/run/types/allure.ts b/run/types/allure.ts index 5872894b7..2c16feeec 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -47,6 +47,7 @@ export type AllureSuiteConfig = | 'Delete Conversation' | 'Delete Message' | 'Hide Note to Self' + | 'Pin/Unpin' | 'Set Nickname' | 'Share to Session'; } diff --git a/run/types/testing.ts b/run/types/testing.ts index 5c6710d6e..e016d3f80 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -129,6 +129,7 @@ export type XPath = | `//android.view.ViewGroup[@resource-id='network.loki.messenger:id/mainContainer'][.//android.widget.TextView[contains(@text,'${string}')]]//androidx.compose.ui.platform.ComposeView[@resource-id='network.loki.messenger:id/profilePictureView']` | `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.view.ViewGroup[@resource-id="network.loki.messenger:id/layout_emoji_container"]` | `//android.view.ViewGroup[@resource-id="network.loki.messenger:id/mainContainer"][.//android.widget.TextView[contains(@text,"${string}")]]//android.widget.TextView[@resource-id="network.loki.messenger:id/reactions_pill_count"][@text="${string}"]` + | `//android.view.ViewGroup[android.widget.TextView[@content-desc='Conversation list item' and @text='${string}']]/android.widget.ImageView[@resource-id='network.loki.messenger:id/iconPinned']` | `//android.widget.LinearLayout[.//android.widget.TextView[@content-desc="Conversation list item" and @text="${string}"]]//android.widget.TextView[@resource-id="network.loki.messenger:id/snippetTextView" and @text="${string}"]` | `//android.widget.TextView[@text="${string}"]` | `//android.widget.TextView[@text="Message"]/parent::android.view.View` @@ -414,6 +415,7 @@ export type AccessibilityId = | 'Time selector' | 'Unban User' | 'Unblock' + | 'Unpin' | 'Untrusted attachment message' | 'Upload' | 'URL' @@ -563,6 +565,7 @@ export type Id = | 'network.loki.messenger:id/theme_option_classic_light' | 'network.loki.messenger:id/thumbnail_load_indicator' | 'network.loki.messenger:id/title' + | 'network.loki.messenger:id/unpinTextView' | 'New direct message' | 'Next' | 'nickname-input' From 283d7e264f7f9e91863e983c8e1a860addcec239 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 25 Feb 2026 15:43:58 +1100 Subject: [PATCH 124/184] fix: quicken pinned assertion logic --- run/test/utils/conversation_order.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts index 45131bd27..0b686ca97 100644 --- a/run/test/utils/conversation_order.ts +++ b/run/test/utils/conversation_order.ts @@ -13,10 +13,23 @@ export const assertPinOrder = ( pinnedNames: string[], afterOrder: string[] ) => { - const expected = [ - ...beforeOrder.filter(n => pinnedNames.includes(n)), - ...beforeOrder.filter(n => !pinnedNames.includes(n)), - ]; + const pinnedSet = new Set(pinnedNames); + const pinnedExpected: string[] = []; + const unpinnedExpected: string[] = []; + for (const name of beforeOrder) { + if (pinnedSet.has(name)) { + pinnedExpected.push(name); + } else { + unpinnedExpected.push(name); + } + } + const expected = [...pinnedExpected, ...unpinnedExpected]; + + if (afterOrder.length !== expected.length) { + throw new Error( + `Conversation count mismatch: expected ${expected.length} conversations but got ${afterOrder.length}` + ); + } for (let i = 0; i < expected.length; i++) { if (afterOrder[i] !== expected[i]) { From 28a31bee96173ea4be597e71e4bbc22e22454747 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Feb 2026 11:18:17 +1100 Subject: [PATCH 125/184] fix: just use deep equality check --- run/test/utils/conversation_order.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts index 0b686ca97..606706eda 100644 --- a/run/test/utils/conversation_order.ts +++ b/run/test/utils/conversation_order.ts @@ -1,3 +1,5 @@ +import { expect } from '@playwright/test'; + import { DeviceWrapper } from '../../types/DeviceWrapper'; // Returns the names of all conversation list items in their current DOM order @@ -25,17 +27,5 @@ export const assertPinOrder = ( } const expected = [...pinnedExpected, ...unpinnedExpected]; - if (afterOrder.length !== expected.length) { - throw new Error( - `Conversation count mismatch: expected ${expected.length} conversations but got ${afterOrder.length}` - ); - } - - for (let i = 0; i < expected.length; i++) { - if (afterOrder[i] !== expected[i]) { - console.log(`Conversation order wrong at position ${i + 1}: expected "${expected[i]}" but got "${afterOrder[i]}". - Full order: [${afterOrder.join(', ')}]`); - throw new Error(`Conversations are not in the correct order after (un)pinning`); - } - } + expect(afterOrder, 'Conversation order is not correct').toEqual(expected); }; From 2a728cf2714cae457892c449dc8828659d010a84 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Feb 2026 11:18:37 +1100 Subject: [PATCH 126/184] chore: update jira links --- run/test/specs/community_emoji_react.spec.ts | 3 --- run/test/specs/disappearing_call.spec.ts | 3 +++ run/test/specs/network_page_refresh_page.spec.ts | 3 --- run/test/specs/user_actions_change_username.spec.ts | 3 --- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/run/test/specs/community_emoji_react.spec.ts b/run/test/specs/community_emoji_react.spec.ts index d17581a4b..2c1b4b3c2 100644 --- a/run/test/specs/community_emoji_react.spec.ts +++ b/run/test/specs/community_emoji_react.spec.ts @@ -18,9 +18,6 @@ bothPlatformsIt({ suite: 'Emoji reacts', }, allureDescription: 'Verifies that an emoji reaction can be sent and is received in a community', - allureLinks: { - android: 'SES-4608', - }, }); async function sendEmojiReactionCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 09ba62b0a..42caceccc 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -28,6 +28,9 @@ bothPlatformsItSeparate({ }, allureDescription: 'Verifies that a call control message disappears as expected in a 1:1 conversation', + allureLinks: { + android: 'SES-5265', + }, }); const time = DISAPPEARING_TIMES.THIRTY_SECONDS; diff --git a/run/test/specs/network_page_refresh_page.spec.ts b/run/test/specs/network_page_refresh_page.spec.ts index 64c2173ec..bcc8010d6 100644 --- a/run/test/specs/network_page_refresh_page.spec.ts +++ b/run/test/specs/network_page_refresh_page.spec.ts @@ -17,9 +17,6 @@ bothPlatformsIt({ parent: 'Network Page', }, allureDescription: `Verifies that the Network Page refreshes and updates the "Last updated" timestamp correctly.`, - allureLinks: { - android: 'SES-4884', - }, }); async function refreshNetworkPage(platform: SupportedPlatformsType, testInfo: TestInfo) { diff --git a/run/test/specs/user_actions_change_username.spec.ts b/run/test/specs/user_actions_change_username.spec.ts index 0a242b628..00d1bad3a 100644 --- a/run/test/specs/user_actions_change_username.spec.ts +++ b/run/test/specs/user_actions_change_username.spec.ts @@ -13,9 +13,6 @@ bothPlatformsIt({ risk: 'medium', countOfDevicesNeeded: 1, testCb: changeUsername, - allureLinks: { - android: 'SES-4277', - }, }); async function changeUsername(platform: SupportedPlatformsType, testInfo: TestInfo) { From 478e24673e3b538e47e3a72e60cdd90f3e9c0f1f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Feb 2026 15:46:27 +1100 Subject: [PATCH 127/184] feat: pro pin limit tests --- run/constants/community.ts | 12 ++ run/test/specs/recovery_banner.spec.ts | 12 +- run/test/specs/user_actions_pin_unpin.spec.ts | 116 +++++++++++++++--- run/test/utils/community.ts | 25 +++- run/types/DeviceWrapper.ts | 4 +- run/types/allure.ts | 2 + run/types/cta.ts | 17 ++- 7 files changed, 152 insertions(+), 36 deletions(-) diff --git a/run/constants/community.ts b/run/constants/community.ts index 3904cc81d..54a2b7d50 100644 --- a/run/constants/community.ts +++ b/run/constants/community.ts @@ -10,6 +10,10 @@ export const communities: Record = { name: 'Testing All The Things!', roomName: 'testing-all-the-things', }, + testOmg: { + link: 'https://test-chat.session.codes/omg?public_key=1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17', + name: 'omg', + }, lokinetUpdates: { link: 'https://open.getsession.org/lokinet-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', name: 'Lokinet Updates', @@ -18,4 +22,12 @@ export const communities: Record = { link: 'https://open.getsession.org/oxen-updates?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', name: 'Session Network Updates', }, + session: { + link: 'https://open.getsession.org/session?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Session', + }, + sessionDev: { + link: 'https://open.getsession.org/session-dev?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238', + name: 'Session Developers Chat', + }, }; diff --git a/run/test/specs/recovery_banner.spec.ts b/run/test/specs/recovery_banner.spec.ts index c71967f64..5cca2fa5e 100644 --- a/run/test/specs/recovery_banner.spec.ts +++ b/run/test/specs/recovery_banner.spec.ts @@ -7,7 +7,7 @@ import { DeviceWrapper } from '../../types/DeviceWrapper'; import { androidIt } from '../../types/sessionIt'; import { ConversationItem, PlusButton } from '../locators/home'; import { RecoveryPhraseContainer, RevealRecoveryPhraseButton } from '../locators/settings'; -import { joinCommunity } from '../utils/community'; +import { joinCommunities, joinCommunity } from '../utils/community'; import { newUser } from '../utils/create_account'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; @@ -87,10 +87,7 @@ async function bannerDisappearsAfterOpened(platform: SupportedPlatformsType, tes return { device }; }); await test.step('Create three conversations, verify banner does not reappear after being opened', async () => { - for (const community of Object.values(communities).slice(0, 3)) { - await joinCommunity(device, community.link, community.name); - await device.navigateBack(); - } + await joinCommunities(device, 3); await bannerShouldShow(device); await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); await device.waitForTextElementToBePresent(new RecoveryPhraseContainer(device)); @@ -109,10 +106,7 @@ async function bannerPersists(platform: SupportedPlatformsType, testInfo: TestIn return { device }; }); await test.step('Create three conversations, verify banner persists after a conversation is deleted', async () => { - for (const community of Object.values(communities).slice(0, 3)) { - await joinCommunity(device, community.link, community.name); - await device.navigateBack(); - } + await joinCommunities(device, 3); await bannerShouldShow(device); await device.longPressConversation(communities.testCommunity.name); await device.clickOnElementAll({ strategy: 'accessibility id', selector: 'Leave' }); // Long press options diff --git a/run/test/specs/user_actions_pin_unpin.spec.ts b/run/test/specs/user_actions_pin_unpin.spec.ts index a36425546..5d467354e 100644 --- a/run/test/specs/user_actions_pin_unpin.spec.ts +++ b/run/test/specs/user_actions_pin_unpin.spec.ts @@ -4,16 +4,19 @@ import { USERNAME } from '@session-foundation/qa-seeder'; import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; -import { ConversationPinnedIcon } from '../locators/home'; -import { joinCommunity } from '../utils/community'; +import { ConversationPinnedIcon, PlusButton } from '../locators/home'; +import { IOSTestContext } from '../utils/capabilities_ios'; +import { joinCommunities } from '../utils/community'; import { assertPinOrder, getConversationOrder } from '../utils/conversation_order'; import { newUser } from '../utils/create_account'; +import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; +import { forceStopAndRestart } from '../utils/utilities'; bothPlatformsIt({ title: 'Pin and unpin conversation', risk: 'medium', - testCb: pinConversationTest, + testCb: pinConversation, countOfDevicesNeeded: 1, allureSuites: { parent: 'User Actions', @@ -23,59 +26,136 @@ bothPlatformsIt({ 'Verifies that pinning moves a conversation to the top of the list and unpinning restores the original order', }); -async function pinConversationTest(platform: SupportedPlatformsType, testInfo: TestInfo) { +bothPlatformsIt({ + title: 'Pinned conversation limit (non Pro)', + risk: 'high', + testCb: nonProPinnedLimit, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Session Pro', + }, + allureDescription: 'Verifies that a standard user can only pin 5 conversations', +}); + +bothPlatformsIt({ + title: 'Pinned conversation limit (Pro)', + risk: 'high', + testCb: proPinnedLimit, + countOfDevicesNeeded: 1, + allureSuites: { + parent: 'Session Pro', + }, + allureDescription: 'Verifies that a Pro user can pin 5+ conversations', +}); + +async function pinConversation(platform: SupportedPlatformsType, testInfo: TestInfo) { + const numCommunities = 2; const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); return { device }; }); - - await test.step('Join two communities', async () => { - await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); - await device.navigateBack(); - await joinCommunity(device, communities.lokinetUpdates.link, communities.lokinetUpdates.name); - await device.navigateBack(); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITIES(numCommunities), async () => { + await joinCommunities(device, numCommunities); }); - let beforeOrder: string[] = []; let toPin = ''; - await test.step('Capture conversation order before pinning', async () => { beforeOrder = await getConversationOrder(device); toPin = beforeOrder[beforeOrder.length - 1]; device.log(`Pinning last conversation: "${toPin}"`); }); - await test.step(`Pin "${toPin}"`, async () => { await device.pinConversation(toPin); }); - await test.step('Assert pinned conversation moved to top', async () => { const afterOrder = await getConversationOrder(device); assertPinOrder(beforeOrder, [toPin], afterOrder); }); - if (platform === 'android') { await test.step('Assert pin icon is visible on pinned conversation', async () => { await device.waitForTextElementToBePresent(new ConversationPinnedIcon(device, toPin)); }); } - await test.step(`Unpin "${toPin}"`, async () => { await device.unpinConversation(toPin); }); - await test.step('Assert order restored after unpinning', async () => { const afterUnpinOrder = await getConversationOrder(device); assertPinOrder(beforeOrder, [], afterUnpinOrder); }); - if (platform === 'android') { await test.step('Assert pin icon is gone after unpinning', async () => { await device.verifyElementNotPresent(new ConversationPinnedIcon(device, toPin)); }); } + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} +async function nonProPinnedLimit(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const numCommunities = 6; + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITIES(numCommunities), async () => { + await joinCommunities(device, numCommunities); + }); + await test.step(TestSteps.USER_ACTIONS.PIN_CONVERSATIONS(numCommunities), async () => { + let pinned = 0; + for (const community of Object.values(communities).slice(0, numCommunities)) { + await device.pinConversation(community.name); + pinned++; + if (pinned < numCommunities) { + await device.waitForTextElementToBePresent(new PlusButton(device)); + await device.verifyNoCTAShows(); + await device + .onAndroid() + .waitForTextElementToBePresent(new ConversationPinnedIcon(device, community.name)); + } else { + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Pinned Conversations CTA'), async () => { + await device.checkCTA('pinnedConversations'); + }); + } + } + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function proPinnedLimit(platform: SupportedPlatformsType, testInfo: TestInfo) { + const iosContext: IOSTestContext = { + sessionProEnabled: 'true', + }; + const numCommunities = 6; + const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const alice = await newUser(device, USERNAME.ALICE); + return { device, alice }; + }); + await makeAccountPro({ user: alice, platform }); + await forceStopAndRestart(device); + await device.dismissCTA(); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITIES(numCommunities), async () => { + await joinCommunities(device, numCommunities); + }); + await test.step(TestSteps.USER_ACTIONS.PIN_CONVERSATIONS(numCommunities), async () => { + for (const community of Object.values(communities).slice(0, numCommunities)) { + await device.pinConversation(community.name); + await device + .onAndroid() + .waitForTextElementToBePresent(new ConversationPinnedIcon(device, community.name)); + await device.waitForTextElementToBePresent(new PlusButton(device)); + } + await device.verifyNoCTAShows(); + }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); }); diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index 4645e52e4..c48302e3c 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -1,11 +1,20 @@ import test from '@playwright/test'; +import { communities } from '../../constants/community'; import { DeviceWrapper } from '../../types/DeviceWrapper'; import { CommunityInput, JoinCommunityButton } from '../locators'; import { ConversationHeaderName, MessageBody } from '../locators/conversation'; import { PlusButton } from '../locators/home'; import { JoinCommunityOption } from '../locators/start_conversation'; +export function assertAdminIsKnown() { + if (!process.env.SOGS_ADMIN_SEED) { + console.error('SOGS_ADMIN_SEED required. In CI this is a GitHub secret.'); + console.error('Locally, set a known admin seed as an env var to run this test.'); + test.skip(); + } +} + export const joinCommunity = async ( device: DeviceWrapper, communityLink: string, @@ -20,10 +29,14 @@ export const joinCommunity = async ( await device.scrollToBottom(); }; -export function assertAdminIsKnown() { - if (!process.env.SOGS_ADMIN_SEED) { - console.error('SOGS_ADMIN_SEED required. In CI this is a GitHub secret.'); - console.error('Locally, set a known admin seed as an env var to run this test.'); - test.skip(); +export const joinCommunities = async (device: DeviceWrapper, number: number) => { + const available = Object.values(communities).length; + if (number > available) { + throw new Error(`joinCommunities: requested ${number} but only ${available} communities have been recorded + Check run/constants/community.ts for more`); } -} + for (const community of Object.values(communities).slice(0, number)) { + await joinCommunity(device, community.link, community.name); + await device.navigateBack(); + } +}; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 1b16ddcff..5781e2140 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1056,8 +1056,8 @@ export class DeviceWrapper { if (elements && elements.length) { const matching = await this.findAsync(elements, async e => { const text = await this.getTextFromElement(e); - const isPartialMatch = text && text.toLowerCase().includes(textToLookFor.toLowerCase()); - return Boolean(isPartialMatch); + const isExactMatch = text && text.toLowerCase() === textToLookFor.toLowerCase(); + return Boolean(isExactMatch); }); return matching || null; diff --git a/run/types/allure.ts b/run/types/allure.ts index 2c16feeec..fd621d9d2 100644 --- a/run/types/allure.ts +++ b/run/types/allure.ts @@ -79,6 +79,7 @@ export const TestSteps = { NEW_CONVERSATION: { NEW_MESSAGE: 'New Message', JOIN_COMMUNITY: 'Join Community', + JOIN_COMMUNITIES: (number: number) => `Join ${number} communities`, }, // Sending things SEND: { @@ -104,6 +105,7 @@ export const TestSteps = { DELETE_FOR_EVERYONE: 'Delete for everyone', GROUPS_ADD_CONTACT: (name: string) => `Invite ${name} to group`, GROUPS_REMOVE_MEMBER: (name: string) => `Remove ${name} from group`, + PIN_CONVERSATIONS: (number: number) => `Attempt to pin ${number} conversations`, }, // Disappearing Messages DISAPPEARING_MESSAGES: { diff --git a/run/types/cta.ts b/run/types/cta.ts index 6e0aa59b1..ecdecc76a 100644 --- a/run/types/cta.ts +++ b/run/types/cta.ts @@ -1,6 +1,11 @@ import { tStripped } from '../localizer/lib'; -export type CTAType = 'alreadyActivated' | 'animatedProfilePicture' | 'donate' | 'longerMessages'; +export type CTAType = + | 'alreadyActivated' + | 'animatedProfilePicture' + | 'donate' + | 'longerMessages' + | 'pinnedConversations'; /** * buttons[0] is the negative/dismiss button (always present); @@ -44,4 +49,14 @@ export const ctaConfigs: Record = { body: tStripped('proAnimatedDisplayPicture'), buttons: [tStripped('close')], }, + pinnedConversations: { + heading: tStripped('upgradeTo'), + body: tStripped('proCallToActionPinnedConversationsMoreThan', { limit: '5' }), + buttons: [tStripped('cancel'), tStripped('theContinue')], + features: [ + tStripped('proFeatureListPinnedConversations'), + tStripped('proFeatureListLongerMessages'), + tStripped('proFeatureListLoadsMore'), + ], + }, }; From 4db538c744393ae70adcb3f990883f22c1485ef3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Feb 2026 17:06:58 +1100 Subject: [PATCH 128/184] chore: copilot review --- run/test/locators/index.ts | 4 ++-- run/types/DeviceWrapper.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index 603d02f33..267b1d1ca 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -314,7 +314,7 @@ export class FirstGif extends LocatorsInterface { export class GIFName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { - // Dates can wildly differ between emulators but it will begin with "Photo taken on" on Android + // Dates can wildly differ between emulators but it will begin with "X taken on" on Android case 'android': return { strategy: 'xpath', @@ -329,7 +329,7 @@ export class GIFName extends LocatorsInterface { export class ImageName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { - // Dates can wildly differ between emulators but it will begin with "GIF taken on" on Android + // Dates can wildly differ between emulators but it will begin with "X taken on" on Android case 'android': return { strategy: 'xpath', diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5781e2140..1c7253279 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1852,7 +1852,6 @@ export class DeviceWrapper { paste: boolean = false ) { const locator = args instanceof LocatorsInterface ? args.build() : args; - const el = await this.waitForTextElementToBePresent({ ...locator }); if (paste) { // Set clipboard, press key-code for instant paste @@ -1872,6 +1871,7 @@ export class DeviceWrapper { await this.clickOnByAccessibilityID('Paste'); } } else { + const el = await this.waitForTextElementToBePresent({ ...locator }); await this.setValueImmediate(textToInput, el.ELEMENT); } } From 003c03670ea64747ac946ae29de7d68bc95fb7aa Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 10:46:51 +1100 Subject: [PATCH 129/184] fix: no playwright noise in allure test body --- playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright.config.ts b/playwright.config.ts index 4794411a8..5e0582a4f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,6 +17,7 @@ const baseReporter: ReporterDescription = [ const allureReporter: ReporterDescription = [ 'allure-playwright', { + detail: false, // No Playwright internal steps in the test body resultsDir: allureResultsDir, categories: [ { From 880e108f0ffeb1eb9e99598282a334100aafbdc2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 11:34:42 +1100 Subject: [PATCH 130/184] fix: give calls more time --- run/test/specs/disappearing_call.spec.ts | 4 ++-- run/test/specs/voice_calls.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 42caceccc..941912652 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -153,13 +153,13 @@ async function disappearingCallMessage1o1Android( strategy: 'id', selector: 'network.loki.messenger:id/callTitle', text: 'Ringing...', - maxWait: 5_000, + maxWait: 10_000, }); await alice1.waitForTextElementToBePresent({ strategy: 'id', selector: 'network.loki.messenger:id/callSubtitle', text: 'Sending Call Offer 2/5', - maxWait: 5_000, + maxWait: 10_000, }); await alice1.clickOnElementById('network.loki.messenger:id/endCallButton'); const callEndTimestamp = Date.now(); diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index c71789ccf..2e2c0c19a 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -203,13 +203,13 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'id', selector: 'network.loki.messenger:id/callTitle', text: 'Ringing...', - maxWait: 5_000, + maxWait: 10_000, }); await alice1.waitForTextElementToBePresent({ strategy: 'id', selector: 'network.loki.messenger:id/callSubtitle', text: 'Sending Call Offer 2/5', - maxWait: 5_000, + maxWait: 10_000, }); }); await alice1.clickOnElementById('network.loki.messenger:id/endCallButton'); From 2983e223b2f2be2e167dac9ee68761517e1eb76d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 11:35:00 +1100 Subject: [PATCH 131/184] fix: before matchAndTapImage, make sure we're on the right screen --- run/types/DeviceWrapper.ts | 11 +++++++++-- run/types/testing.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 1c7253279..2091ab7f6 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1961,8 +1961,11 @@ export class DeviceWrapper { if (this.isIOS()) { await this.clickOnElementAll(new AttachmentsButton(this)); await this.clickOnElementAll(new ImagesFolderButton(this)); - await sleepFor(1000); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); + await this.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'Recents', + }); await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, testImage @@ -2004,11 +2007,11 @@ export class DeviceWrapper { // iOS files are pre-loaded on simulator creation, no need to push await this.clickOnElementAll(new AttachmentsButton(this)); await this.clickOnElementAll(new ImagesFolderButton(this)); - await sleepFor(100); await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access', }); + await this.waitForTextElementToBePresent({ strategy: 'accessibility id', selector: 'Recents' }); // A video can't be matched by its thumbnail so we use a video thumbnail file await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeCell` }, @@ -2227,6 +2230,10 @@ export class DeviceWrapper { // iOS files are pre-loaded on simulator creation, no need to push if (this.isIOS()) { await this.modalPopup({ strategy: 'accessibility id', selector: 'Allow Full Access' }); + await this.waitForTextElementToBePresent({ + strategy: 'accessibility id', + selector: 'Collections', + }); await this.matchAndTapImage( { strategy: 'xpath', selector: `//XCUIElementTypeImage` }, uploadPicture diff --git a/run/types/testing.ts b/run/types/testing.ts index e016d3f80..28a2f0b35 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -213,6 +213,7 @@ export type AccessibilityId = | 'Clear all' | 'Close' | 'Close button' + | 'Collections' | 'Community invitation' | 'Community Message Requests' | 'Configuration message' From 6e5fdb70b79298e29cae1ea7802616cfc058502a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 11:47:58 +1100 Subject: [PATCH 132/184] fix: iOS adjustments --- run/test/locators/onboarding.ts | 4 ++-- run/test/locators/settings.ts | 23 +++++++++++++++++-- ...r_actions_animated_profile_picture.spec.ts | 14 +++++------ run/types/testing.ts | 5 ++-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/run/test/locators/onboarding.ts b/run/test/locators/onboarding.ts index ef6544b98..9f312e492 100644 --- a/run/test/locators/onboarding.ts +++ b/run/test/locators/onboarding.ts @@ -132,7 +132,7 @@ export class PrivacyPolicyButton extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Privacy Policy', + selector: 'https://getsession.org/privacy-policy', } as const; } } @@ -200,7 +200,7 @@ export class TermsOfServiceButton extends LocatorsInterface { case 'ios': return { strategy: 'accessibility id', - selector: 'Terms of Service', + selector: 'https://getsession.org/terms-of-service', } as const; } } diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index a1f625077..36740dc06 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -1,3 +1,4 @@ +import { tStripped } from '../../localizer/lib'; import { StrategyExtractionObj } from '../../types/testing'; import { LocatorsInterface } from './index'; @@ -203,6 +204,24 @@ export class PrivacyMenuItem extends LocatorsInterface { } } +export class ProAnimatedDisplayPictureModalDescription extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: 'id', + selector: 'pro-badge-text', + text: tStripped('proAnimatedDisplayPictureModalDescription'), + } as const; + case 'ios': + return { + strategy: 'accessibility id', + selector: 'users can upload GIFs', + } as const; + } + } +} + export class RecoveryPasswordMenuItem extends LocatorsInterface { public build() { switch (this.platform) { @@ -271,7 +290,6 @@ export class SaveNameChangeButton extends LocatorsInterface { } } } - export class SaveProfilePictureButton extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -305,6 +323,7 @@ export class SelectAppIcon extends LocatorsInterface { } } } + export class SettingsModalsEnableButton extends LocatorsInterface { public build() { switch (this.platform) { @@ -321,7 +340,6 @@ export class SettingsModalsEnableButton extends LocatorsInterface { } } } - export class UserAvatar extends LocatorsInterface { public build() { switch (this.platform) { @@ -338,6 +356,7 @@ export class UserAvatar extends LocatorsInterface { } } } + export class UserSettings extends LocatorsInterface { public build() { return { diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 1f1e0153b..abec51fd2 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -1,13 +1,17 @@ import { test, type TestInfo } from '@playwright/test'; -import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { CloseSettings } from '../locators'; import { ConversationSettings, MessageBody } from '../locators/conversation'; import { ConversationItem } from '../locators/home'; -import { PathMenuItem, UserAvatar, UserSettings } from '../locators/settings'; +import { + PathMenuItem, + ProAnimatedDisplayPictureModalDescription, + UserAvatar, + UserSettings, +} from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; import { IOSTestContext } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; @@ -90,11 +94,7 @@ async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestI await test.step('Verify Pro Activated CTA', async () => { await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new UserAvatar(device)); - await device.clickOnElementAll({ - strategy: 'id', - selector: 'pro-badge-text', - text: tStripped('proAnimatedDisplayPictureModalDescription'), - }); + await device.clickOnElementAll(new ProAnimatedDisplayPictureModalDescription(device)); await verifyPageScreenshot(device, platform, 'cta_pro_activated', testInfo); await device.checkCTA('alreadyActivated'); }); diff --git a/run/types/testing.ts b/run/types/testing.ts index 28a2f0b35..7ca517852 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -295,6 +295,8 @@ export type AccessibilityId = | 'Hide Note to Self' | 'Hide recovery password button' | 'Hide Recovery Password Permanently' + | 'https://getsession.org/privacy-policy' + | 'https://getsession.org/terms-of-service' | 'Image picker' | 'Images folder' | 'Invite' @@ -365,7 +367,6 @@ export type AccessibilityId = | 'Photos' | 'Pin' | 'Please enter a shorter group name' - | 'Privacy Policy' | 'rate-app-button' | 'Read Receipts - Switch' | 'Recents' @@ -411,7 +412,6 @@ export type AccessibilityId = | 'space' | 'Staking reward pool amount' | 'TabBarItemTitle' - | 'Terms of Service' | 'test_file, pdf' | 'Time selector' | 'Unban User' @@ -422,6 +422,7 @@ export type AccessibilityId = | 'URL' | 'Username' | 'Username input' + | 'users can upload GIFs' | 'User settings' | 'Version warning banner' | 'Videos' From bbccdf9a268638708c1d030ab427f9a1669b89ec Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 12:00:21 +1100 Subject: [PATCH 133/184] refactor: centralize pro context declaration --- run/test/specs/message_length.spec.ts | 7 ++---- ...r_actions_animated_profile_picture.spec.ts | 22 +++++-------------- run/test/specs/user_actions_pin_unpin.spec.ts | 12 +++------- run/test/utils/capabilities_ios.ts | 2 ++ 4 files changed, 12 insertions(+), 31 deletions(-) diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 006cdc732..5e8661bdc 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -14,7 +14,7 @@ import { import { CTAButtonNegative } from '../locators/global'; import { PlusButton } from '../locators/home'; import { EnterAccountID, NewMessageOption, NextButton } from '../locators/start_conversation'; -import { IOSTestContext } from '../utils/capabilities_ios'; +import { IOS_PRO_CONTEXT } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; @@ -87,11 +87,8 @@ for (const testCase of messageLengthTestCases) { }, allureDescription: `Verifies message length behavior at ${testCase.length} characters - ${testCase.description} (${proSuffix})`, testCb: async (platform: SupportedPlatformsType, testInfo: TestInfo) => { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index abec51fd2..ccd8cad6c 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -13,7 +13,7 @@ import { UserSettings, } from '../locators/settings'; import { open_Alice1_Bob1_friends } from '../state_builder'; -import { IOSTestContext } from '../utils/capabilities_ios'; +import { IOS_PRO_CONTEXT } from '../utils/capabilities_ios'; import { newUser } from '../utils/create_account'; import { makeAccountPro } from '../utils/mock_pro'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; @@ -63,11 +63,8 @@ bothPlatformsIt({ }); async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); await newUser(device, USERNAME.ALICE, { saveUserData: false }); return { device }; }); @@ -81,11 +78,8 @@ async function nonProAnimatedDP(platform: SupportedPlatformsType, testInfo: Test }); } async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); @@ -104,11 +98,8 @@ async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestI } async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); @@ -126,15 +117,12 @@ async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInf } async function proAnimatedDPShows(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const { devices, prebuilt } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return await open_Alice1_Bob1_friends({ platform, focusFriendsConvo: false, testInfo, - iOSContext: iosContext, + iOSContext: IOS_PRO_CONTEXT, }); }); const { alice1, bob1 } = devices; diff --git a/run/test/specs/user_actions_pin_unpin.spec.ts b/run/test/specs/user_actions_pin_unpin.spec.ts index 5d467354e..a8414eac7 100644 --- a/run/test/specs/user_actions_pin_unpin.spec.ts +++ b/run/test/specs/user_actions_pin_unpin.spec.ts @@ -5,7 +5,7 @@ import { communities } from '../../constants/community'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { ConversationPinnedIcon, PlusButton } from '../locators/home'; -import { IOSTestContext } from '../utils/capabilities_ios'; +import { IOS_PRO_CONTEXT } from '../utils/capabilities_ios'; import { joinCommunities } from '../utils/community'; import { assertPinOrder, getConversationOrder } from '../utils/conversation_order'; import { newUser } from '../utils/create_account'; @@ -95,12 +95,9 @@ async function pinConversation(platform: SupportedPlatformsType, testInfo: TestI } async function nonProPinnedLimit(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const numCommunities = 6; const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); await newUser(device, USERNAME.ALICE, { saveUserData: false }); return { device }; }); @@ -131,12 +128,9 @@ async function nonProPinnedLimit(platform: SupportedPlatformsType, testInfo: Tes } async function proPinnedLimit(platform: SupportedPlatformsType, testInfo: TestInfo) { - const iosContext: IOSTestContext = { - sessionProEnabled: 'true', - }; const numCommunities = 6; const { device, alice } = await test.step(TestSteps.SETUP.NEW_USER, async () => { - const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, iosContext); + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo, IOS_PRO_CONTEXT); const alice = await newUser(device, USERNAME.ALICE); return { device, alice }; }); diff --git a/run/test/utils/capabilities_ios.ts b/run/test/utils/capabilities_ios.ts index aca51480b..67b756fa8 100644 --- a/run/test/utils/capabilities_ios.ts +++ b/run/test/utils/capabilities_ios.ts @@ -12,6 +12,8 @@ export type IOSTestContext = { sessionProEnabled?: string; }; +export const IOS_PRO_CONTEXT: IOSTestContext = { sessionProEnabled: 'true' }; + const iosPathPrefix = process.env.IOS_APP_PATH_PREFIX; export const iOSBundleId = 'com.loki-project.loki-messenger'; From c147791f5448630ad546ec5e4b6c8a44a39d9533 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 14:04:39 +1100 Subject: [PATCH 134/184] feat: add verify wrapper for playwright's expect --- run/test/utils/conversation_order.ts | 5 ++-- run/test/utils/utilities.ts | 42 ++++++++++++++++++++++++++++ run/types/DeviceWrapper.ts | 8 +++--- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts index 606706eda..6d9064961 100644 --- a/run/test/utils/conversation_order.ts +++ b/run/test/utils/conversation_order.ts @@ -1,6 +1,5 @@ -import { expect } from '@playwright/test'; - import { DeviceWrapper } from '../../types/DeviceWrapper'; +import { verify } from './utilities'; // Returns the names of all conversation list items in their current DOM order export const getConversationOrder = async (device: DeviceWrapper): Promise => { @@ -27,5 +26,5 @@ export const assertPinOrder = ( } const expected = [...pinnedExpected, ...unpinnedExpected]; - expect(afterOrder, 'Conversation order is not correct').toEqual(expected); + verify(afterOrder, 'Conversation order is not correct').toEqual(expected); }; diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index c21434f91..17189b15b 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -1,3 +1,4 @@ +import { expect } from '@playwright/test'; import { exec as execNotPromised } from 'child_process'; import * as fs from 'fs'; import { pick } from 'lodash'; @@ -150,3 +151,44 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise // Ensure we're on the home screen again await device.waitForTextElementToBePresent(new PlusButton(device)); } + +/** + * Drop-in replacement for Playwright's `expect()` that keeps Allure reports clean. + * + * Playwright dumps the full diff (received vs expected) into the error message, which + * ends up verbatim in Allure — too technical for customers. `verify()` catches + * assertion errors and rethrows with only the human-readable `message`, preserving the diffs + * in the runner logs. + * + * @param actual - The value being asserted + * @param message - Business-readable failure message — this is all Allure will show on failure. + * + * @example + * verify(messages, 'Conversation messages are in the wrong order').toEqual(expected); + * verify(isVisible, 'Blocked user banner should not be visible').not.toBe(true); + */ +export function verify(actual: T, message: string) { + const matchers = expect(actual, message); + + function wrapMatchers(obj: typeof matchers): typeof matchers { + return new Proxy(obj, { + get(target, prop: string | symbol) { + const val = Reflect.get(target, prop, target); + if (prop === 'not') return wrapMatchers(val as typeof matchers); + if (typeof val === 'function') { + return (...args: unknown[]) => { + try { + return (val as (...a: unknown[]) => unknown).apply(target, args); + } catch { + console.log(`${message}\n actual: `, actual, '\n expected:', args[0]); + throw new Error(message); + } + }; + } + return val; + }, + }); + } + + return wrapMatchers(matchers); +} diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2091ab7f6..5c3575d80 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1,7 +1,7 @@ import type { Constraints, DefaultCreateSessionResult } from '@appium/types'; import { getImageOccurrence } from '@appium/opencv'; -import { expect, TestInfo } from '@playwright/test'; +import { TestInfo } from '@playwright/test'; import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver'; import { W3CUiautomator2DriverCaps } from 'appium-uiautomator2-driver/build/lib/types'; import { W3CXCUITestDriverCaps, XCUITestDriver } from 'appium-xcuitest-driver/build/lib/driver'; @@ -78,7 +78,7 @@ import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; import { SupportedPlatformsType } from '../test/utils/open_app'; -import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; +import { isDeviceAndroid, isDeviceIOS, runScriptAndLog, verify } from '../test/utils/utilities'; import { CTAConfig, ctaConfigs, CTAType } from './cta'; import { AccessibilityId, @@ -1887,7 +1887,7 @@ export class DeviceWrapper { ) { const el = await this.waitForTextElementToBePresent(element); const received = await this.getAttribute(attribute, el.ELEMENT); - expect(received, 'Element attribute value mismatch').toBe(value); + verify(received, 'Element attribute value mismatch').toBe(value); } public async disappearRadioButtonSelected( @@ -2682,7 +2682,7 @@ export class DeviceWrapper { for (let i = 0; i < SAMPLE_SIZE; i++) { colors.add(await this.getElementPixelColor(locator)); } - expect( + verify( colors.size, `Expected element to be animated but detected 1 unique color: ${[...colors][0]}` ).toBeGreaterThan(1); From d6cd11f72fdb5361bc25ef9552ab133ff612ab00 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 14:35:09 +1100 Subject: [PATCH 135/184] fix: keep patching ios tests --- run/screenshots/ios/cta_pro_activated.png | 3 +++ run/test/locators/settings.ts | 3 ++- ...r_actions_animated_profile_picture.spec.ts | 5 +++-- run/test/utils/conversation_order.ts | 4 ++-- run/test/utils/index.ts | 3 ++- run/test/utils/utilities.ts | 8 +++---- run/types/DeviceWrapper.ts | 21 ++++++++++++------- run/types/testing.ts | 2 +- 8 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 run/screenshots/ios/cta_pro_activated.png diff --git a/run/screenshots/ios/cta_pro_activated.png b/run/screenshots/ios/cta_pro_activated.png new file mode 100644 index 000000000..7b21ed551 --- /dev/null +++ b/run/screenshots/ios/cta_pro_activated.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:315676b8601144108703f484f350fde62b081edb3df956b787dab4039c60aeb2 +size 1420623 diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index 36740dc06..8e6fdd56a 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -216,7 +216,7 @@ export class ProAnimatedDisplayPictureModalDescription extends LocatorsInterface case 'ios': return { strategy: 'accessibility id', - selector: 'users can upload GIFs', + selector: ' users can upload GIFs', // Yes this is an intentional whitespace } as const; } } @@ -352,6 +352,7 @@ export class UserAvatar extends LocatorsInterface { return { strategy: 'accessibility id', selector: 'User settings', + text: 'Profile picture', // There's more than one User settings so this is to specify the avatar } as const; } } diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index ccd8cad6c..231ca7b99 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -3,7 +3,7 @@ import { test, type TestInfo } from '@playwright/test'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; -import { CloseSettings } from '../locators'; +import { ChangeProfilePictureButton, CloseSettings } from '../locators'; import { ConversationSettings, MessageBody } from '../locators/conversation'; import { ConversationItem } from '../locators/home'; import { @@ -88,9 +88,10 @@ async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestI await test.step('Verify Pro Activated CTA', async () => { await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new UserAvatar(device)); + await device.waitForTextElementToBePresent(new ChangeProfilePictureButton(device)); await device.clickOnElementAll(new ProAnimatedDisplayPictureModalDescription(device)); - await verifyPageScreenshot(device, platform, 'cta_pro_activated', testInfo); await device.checkCTA('alreadyActivated'); + await verifyPageScreenshot(device, platform, 'cta_pro_activated', testInfo); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts index 6d9064961..4292eb6b8 100644 --- a/run/test/utils/conversation_order.ts +++ b/run/test/utils/conversation_order.ts @@ -1,5 +1,5 @@ import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { verify } from './utilities'; +import { assert } from './'; // Returns the names of all conversation list items in their current DOM order export const getConversationOrder = async (device: DeviceWrapper): Promise => { @@ -26,5 +26,5 @@ export const assertPinOrder = ( } const expected = [...pinnedExpected, ...unpinnedExpected]; - verify(afterOrder, 'Conversation order is not correct').toEqual(expected); + assert(afterOrder, 'Conversation order is not correct').toEqual(expected); }; diff --git a/run/test/utils/index.ts b/run/test/utils/index.ts index 66e353af7..277d81b1d 100644 --- a/run/test/utils/index.ts +++ b/run/test/utils/index.ts @@ -1,5 +1,6 @@ import { clickOnCoordinates } from './click_by_coordinates'; import { runOnlyOnAndroid, runOnlyOnIOS } from './run_on'; import { sleepFor } from './sleep_for'; +import { assert } from './utilities'; -export { sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; +export { assert, sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index 17189b15b..25625d322 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -156,7 +156,7 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise * Drop-in replacement for Playwright's `expect()` that keeps Allure reports clean. * * Playwright dumps the full diff (received vs expected) into the error message, which - * ends up verbatim in Allure — too technical for customers. `verify()` catches + * ends up verbatim in Allure — too technical for customers. `assert()` catches * assertion errors and rethrows with only the human-readable `message`, preserving the diffs * in the runner logs. * @@ -164,10 +164,10 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise * @param message - Business-readable failure message — this is all Allure will show on failure. * * @example - * verify(messages, 'Conversation messages are in the wrong order').toEqual(expected); - * verify(isVisible, 'Blocked user banner should not be visible').not.toBe(true); + * assert(messages, 'Conversation messages are in the wrong order').toEqual(expected); + * assert(isVisible, 'Blocked user banner should not be visible').not.toBe(true); */ -export function verify(actual: T, message: string) { +export function assert(actual: T, message: string) { const matchers = expect(actual, message); function wrapMatchers(obj: typeof matchers): typeof matchers { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 5c3575d80..390397b06 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -73,12 +73,12 @@ import { VersionNumber, } from '../test/locators/settings'; import { EnterAccountID, NewMessageOption, NextButton } from '../test/locators/start_conversation'; -import { clickOnCoordinates, sleepFor } from '../test/utils'; +import { assert, clickOnCoordinates, sleepFor } from '../test/utils'; import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; import { SupportedPlatformsType } from '../test/utils/open_app'; -import { isDeviceAndroid, isDeviceIOS, runScriptAndLog, verify } from '../test/utils/utilities'; +import { isDeviceAndroid, isDeviceIOS, runScriptAndLog } from '../test/utils/utilities'; import { CTAConfig, ctaConfigs, CTAType } from './cta'; import { AccessibilityId, @@ -1887,7 +1887,7 @@ export class DeviceWrapper { ) { const el = await this.waitForTextElementToBePresent(element); const received = await this.getAttribute(attribute, el.ELEMENT); - verify(received, 'Element attribute value mismatch').toBe(value); + assert(received, 'Element attribute value mismatch').toBe(value); } public async disappearRadioButtonSelected( @@ -2605,10 +2605,15 @@ export class DeviceWrapper { const actualHeading = await this.getTextFromElement(elHeading); this.assertTextMatches(actualHeading, heading, 'CTA heading'); - // CTA body - const elBody = await this.waitForTextElementToBePresent(new CTABody(this)); - const actualBody = await this.getTextFromElement(elBody); - this.assertTextMatches(actualBody, body, 'CTA body'); + // iOS may split the body around inline images, producing multiple cta-body elements. + // Wait for the first, then find all and check that the expected text appears in any of them. + await this.waitForTextElementToBePresent(new CTABody(this)); + const { strategy, selector } = new CTABody(this).build(); + const bodyElements = await this.findElements(strategy, selector, true); + const bodyTexts = await Promise.all(bodyElements.map(el => this.getTextFromElement(el))); + const matchingText = + bodyTexts.find(t => this.sanitizeString(t) === this.sanitizeString(body)) ?? bodyTexts[0]; + this.assertTextMatches(matchingText, body, 'CTA body'); // CTA features if present if (features) { @@ -2682,7 +2687,7 @@ export class DeviceWrapper { for (let i = 0; i < SAMPLE_SIZE; i++) { colors.add(await this.getElementPixelColor(locator)); } - verify( + assert( colors.size, `Expected element to be animated but detected 1 unique color: ${[...colors][0]}` ).toBeGreaterThan(1); diff --git a/run/types/testing.ts b/run/types/testing.ts index 7ca517852..f6f6d6fd6 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -422,7 +422,7 @@ export type AccessibilityId = | 'URL' | 'Username' | 'Username input' - | 'users can upload GIFs' + | ' users can upload GIFs' // Yes this is an intentional whitespace | 'User settings' | 'Version warning banner' | 'Videos' From 73265d758840ca7646aca9b5a3ed8792cff9987e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 27 Feb 2026 15:05:36 +1100 Subject: [PATCH 136/184] fix: more android tests --- run/test/specs/disappearing_call.spec.ts | 4 ++-- run/test/specs/group_tests_add_contact.spec.ts | 2 +- run/test/specs/voice_calls.spec.ts | 4 ++-- run/test/utils/failure_artifacts.ts | 2 +- run/types/DeviceWrapper.ts | 8 +++++++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/run/test/specs/disappearing_call.spec.ts b/run/test/specs/disappearing_call.spec.ts index 941912652..42caceccc 100644 --- a/run/test/specs/disappearing_call.spec.ts +++ b/run/test/specs/disappearing_call.spec.ts @@ -153,13 +153,13 @@ async function disappearingCallMessage1o1Android( strategy: 'id', selector: 'network.loki.messenger:id/callTitle', text: 'Ringing...', - maxWait: 10_000, + maxWait: 5_000, }); await alice1.waitForTextElementToBePresent({ strategy: 'id', selector: 'network.loki.messenger:id/callSubtitle', text: 'Sending Call Offer 2/5', - maxWait: 10_000, + maxWait: 5_000, }); await alice1.clickOnElementById('network.loki.messenger:id/endCallButton'); const callEndTimestamp = Date.now(); diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index f1958c96e..c5c4cf09e 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -92,7 +92,7 @@ async function addContactToGroupHistory(platform: SupportedPlatformsType, testIn await Promise.all( [alice1, bob1, charlie1].map(device => device.waitForControlMessageToBePresent( - tStripped('groupMemberNew', { name: USERNAME.DRACULA }) + tStripped('groupMemberInvitedHistory', { name: USERNAME.DRACULA }) ) ) ); diff --git a/run/test/specs/voice_calls.spec.ts b/run/test/specs/voice_calls.spec.ts index 2e2c0c19a..c71789ccf 100644 --- a/run/test/specs/voice_calls.spec.ts +++ b/run/test/specs/voice_calls.spec.ts @@ -203,13 +203,13 @@ async function voiceCallAndroid(platform: SupportedPlatformsType, testInfo: Test strategy: 'id', selector: 'network.loki.messenger:id/callTitle', text: 'Ringing...', - maxWait: 10_000, + maxWait: 5_000, }); await alice1.waitForTextElementToBePresent({ strategy: 'id', selector: 'network.loki.messenger:id/callSubtitle', text: 'Sending Call Offer 2/5', - maxWait: 10_000, + maxWait: 5_000, }); }); await alice1.clickOnElementById('network.loki.messenger:id/endCallButton'); diff --git a/run/test/utils/failure_artifacts.ts b/run/test/utils/failure_artifacts.ts index 9f82121c3..2ec35c68f 100644 --- a/run/test/utils/failure_artifacts.ts +++ b/run/test/utils/failure_artifacts.ts @@ -240,7 +240,7 @@ async function collectLogBuffer( return null; } -const MAX_LOG_BYTES = 1024 * 1024; // 1 MB — tail beyond this to keep reports lean +const MAX_LOG_BYTES = 512 * 1024; // 512kB — tail beyond this to keep reports lean function tailBuffer(raw: Buffer): Buffer { if (raw.length <= MAX_LOG_BYTES) return raw; diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 390397b06..c269d9cf4 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1056,7 +1056,13 @@ export class DeviceWrapper { if (elements && elements.length) { const matching = await this.findAsync(elements, async e => { const text = await this.getTextFromElement(e); - const isExactMatch = text && text.toLowerCase() === textToLookFor.toLowerCase(); + // Strip LTR/RTL markers and other whitespace nonsense + const normalize = (s: string) => + s + .replace(/[\u200e\u200f\u202a-\u202e]/g, '') + .trim() + .toLowerCase(); + const isExactMatch = text && normalize(text) === normalize(textToLookFor); return Boolean(isExactMatch); }); From bc2746d34dda2180bdb2f803cb5f4f5ac2590375 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:02:37 +0000 Subject: [PATCH 137/184] chore(deps): bump the monthly-updates group with 15 updates Bumps the monthly-updates group with 15 updates: | Package | From | To | | --- | --- | --- | | [appium-uiautomator2-driver](https://github.com/appium/appium-uiautomator2-driver) | `6.8.0` | `7.0.0` | | [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver) | `10.21.2` | `10.24.1` | | [dotenv](https://github.com/motdotla/dotenv) | `17.2.4` | `17.3.1` | | [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) | `4.17.23` | `4.17.24` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.3` | `25.3.3` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.55.0` | `8.56.1` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.55.0` | `8.56.1` | | [allure-commandline](https://github.com/allure-framework/allure-npm) | `2.36.0` | `2.37.0` | | [allure-js-commons](https://github.com/allure-framework/allure-js/tree/HEAD/packages/allure-js-commons) | `3.4.5` | `3.5.0` | | [eslint](https://github.com/eslint/eslint) | `10.0.0` | `10.0.2` | | [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) | `5.5.0` | `5.6.0` | | [glob](https://github.com/isaacs/node-glob) | `13.0.2` | `13.0.6` | | [globals](https://github.com/sindresorhus/globals) | `17.3.0` | `17.4.0` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.55.0` | `8.56.1` | | [undici](https://github.com/nodejs/undici) | `7.21.0` | `7.22.0` | Updates `appium-uiautomator2-driver` from 6.8.0 to 7.0.0 - [Release notes](https://github.com/appium/appium-uiautomator2-driver/releases) - [Changelog](https://github.com/appium/appium-uiautomator2-driver/blob/master/CHANGELOG.md) - [Commits](https://github.com/appium/appium-uiautomator2-driver/compare/v6.8.0...v7.0.0) Updates `appium-xcuitest-driver` from 10.21.2 to 10.24.1 - [Release notes](https://github.com/appium/appium-xcuitest-driver/releases) - [Changelog](https://github.com/appium/appium-xcuitest-driver/blob/master/CHANGELOG.md) - [Commits](https://github.com/appium/appium-xcuitest-driver/compare/v10.21.2...v10.24.1) Updates `dotenv` from 17.2.4 to 17.3.1 - [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/motdotla/dotenv/compare/v17.2.4...v17.3.1) Updates `@types/lodash` from 4.17.23 to 4.17.24 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Updates `@types/node` from 25.2.3 to 25.3.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@typescript-eslint/eslint-plugin` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/parser) Updates `allure-commandline` from 2.36.0 to 2.37.0 - [Release notes](https://github.com/allure-framework/allure-npm/releases) - [Commits](https://github.com/allure-framework/allure-npm/compare/2.36.0...2.37.0) Updates `allure-js-commons` from 3.4.5 to 3.5.0 - [Release notes](https://github.com/allure-framework/allure-js/releases) - [Commits](https://github.com/allure-framework/allure-js/commits/v3.5.0/packages/allure-js-commons) Updates `eslint` from 10.0.0 to 10.0.2 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.0.0...v10.0.2) Updates `eslint-plugin-perfectionist` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases) - [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md) - [Commits](https://github.com/azat-io/eslint-plugin-perfectionist/compare/v5.5.0...v5.6.0) Updates `glob` from 13.0.2 to 13.0.6 - [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md) - [Commits](https://github.com/isaacs/node-glob/compare/v13.0.2...v13.0.6) Updates `globals` from 17.3.0 to 17.4.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.3.0...v17.4.0) Updates `typescript-eslint` from 8.55.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint) Updates `undici` from 7.21.0 to 7.22.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.21.0...v7.22.0) --- updated-dependencies: - dependency-name: appium-uiautomator2-driver dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: monthly-updates - dependency-name: appium-xcuitest-driver dependency-version: 10.24.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: dotenv dependency-version: 17.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: "@types/lodash" dependency-version: 4.17.24 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: "@types/node" dependency-version: 25.3.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: "@typescript-eslint/parser" dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: allure-commandline dependency-version: 2.37.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: allure-js-commons dependency-version: 3.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: eslint dependency-version: 10.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: eslint-plugin-perfectionist dependency-version: 5.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: glob dependency-version: 13.0.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: monthly-updates - dependency-name: globals dependency-version: 17.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: typescript-eslint dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates - dependency-name: undici dependency-version: 7.22.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: monthly-updates ... Signed-off-by: dependabot[bot] --- package.json | 30 +- pnpm-lock.yaml | 725 +++++++++++++++++++++++++------------------------ 2 files changed, 380 insertions(+), 375 deletions(-) diff --git a/package.json b/package.json index 7b4aa28d0..055fac0d3 100644 --- a/package.json +++ b/package.json @@ -30,22 +30,22 @@ "@eslint/js": "^10.0.1", "@types/fs-extra": "^11.0.4", "@types/gh-pages": "^6.1.0", - "@types/lodash": "^4.17.23", - "@types/node": "^25.2.3", + "@types/lodash": "^4.17.24", + "@types/node": "^25.3.3", "@types/sinon": "^21.0.0", - "@typescript-eslint/eslint-plugin": "^8.55.0", - "@typescript-eslint/parser": "^8.55.0", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", "@wdio/types": "^9.24.0", - "allure-commandline": "^2.36.0", - "allure-js-commons": "^3.4.5", + "allure-commandline": "^2.37.0", + "allure-js-commons": "^3.5.0", "allure-playwright": "^3.4.5", - "eslint": "^10.0.0", - "eslint-plugin-perfectionist": "^5.5.0", + "eslint": "^10.0.2", + "eslint-plugin-perfectionist": "^5.6.0", "fs-extra": "^11.3.3", "fuse.js": "^7.1.0", "gh-pages": "^6.3.0", - "glob": "^13.0.2", - "globals": "^17.3.0", + "glob": "^13.0.6", + "globals": "^17.4.0", "lodash": "^4.17.23", "looks-same": "^10.0.1", "png-js": "^1.0.0", @@ -56,8 +56,8 @@ "ssim.js": "^3.5.0", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.55.0", - "undici": "^7.21.0", + "typescript-eslint": "^8.56.1", + "undici": "^7.22.0", "uuid": "^13.0.0" }, "license": "MIT", @@ -73,9 +73,9 @@ "@session-foundation/playwright-reporter": "^0.0.8", "@session-foundation/qa-seeder": "^0.1.26", "appium": "^3.2.0", - "appium-uiautomator2-driver": "^6.8.0", - "appium-xcuitest-driver": "^10.21.2", - "dotenv": "^17.2.4" + "appium-uiautomator2-driver": "^7.0.0", + "appium-xcuitest-driver": "^10.24.1", + "dotenv": "^17.3.1" }, "packageManager": "pnpm@10.28.1", "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce24ef4f2..e5934380f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,14 +44,14 @@ importers: specifier: ^3.2.0 version: 3.2.0 appium-uiautomator2-driver: - specifier: ^6.8.0 - version: 6.8.0(patch_hash=8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121)(appium@3.2.0) + specifier: ^7.0.0 + version: 7.0.0(patch_hash=8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121)(appium@3.2.0) appium-xcuitest-driver: - specifier: ^10.21.2 - version: 10.21.2(appium@3.2.0) + specifier: ^10.24.1 + version: 10.24.1(appium@3.2.0) dotenv: - specifier: ^17.2.4 - version: 17.2.4 + specifier: ^17.3.1 + version: 17.3.1 devDependencies: '@appium/execute-driver-plugin': specifier: ^5.1.0 @@ -67,7 +67,7 @@ importers: version: 1.2.0 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.0) + version: 10.0.1(eslint@10.0.2) '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -75,38 +75,38 @@ importers: specifier: ^6.1.0 version: 6.1.0 '@types/lodash': - specifier: ^4.17.23 - version: 4.17.23 + specifier: ^4.17.24 + version: 4.17.24 '@types/node': - specifier: ^25.2.3 - version: 25.2.3 + specifier: ^25.3.3 + version: 25.3.3 '@types/sinon': specifier: ^21.0.0 version: 21.0.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.55.0 - version: 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2)(typescript@5.9.3))(eslint@10.0.2)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.55.0 - version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.2)(typescript@5.9.3) '@wdio/types': specifier: ^9.24.0 version: 9.24.0 allure-commandline: - specifier: ^2.36.0 - version: 2.36.0 + specifier: ^2.37.0 + version: 2.37.0 allure-js-commons: - specifier: ^3.4.5 - version: 3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) + specifier: ^3.5.0 + version: 3.5.0(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)) allure-playwright: specifier: ^3.4.5 version: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) eslint: - specifier: ^10.0.0 - version: 10.0.0 + specifier: ^10.0.2 + version: 10.0.2 eslint-plugin-perfectionist: - specifier: ^5.5.0 - version: 5.5.0(eslint@10.0.0)(typescript@5.9.3) + specifier: ^5.6.0 + version: 5.6.0(eslint@10.0.2)(typescript@5.9.3) fs-extra: specifier: ^11.3.3 version: 11.3.3 @@ -117,11 +117,11 @@ importers: specifier: ^6.3.0 version: 6.3.0 glob: - specifier: ^13.0.2 - version: 13.0.2 + specifier: ^13.0.6 + version: 13.0.6 globals: - specifier: ^17.3.0 - version: 17.3.0 + specifier: ^17.4.0 + version: 17.4.0 lodash: specifier: ^4.17.23 version: 4.17.23 @@ -148,16 +148,16 @@ importers: version: 3.5.0 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@25.2.3)(typescript@5.9.3) + version: 10.9.2(@types/node@25.3.3)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.55.0 - version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.2)(typescript@5.9.3) undici: - specifier: ^7.21.0 - version: 7.21.0 + specifier: ^7.22.0 + version: 7.22.0 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -249,8 +249,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.23.1': - resolution: {integrity: sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==} + '@eslint/config-array@0.23.2': + resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/config-helpers@0.5.2': @@ -270,8 +270,8 @@ packages: eslint: optional: true - '@eslint/object-schema@3.0.1': - resolution: {integrity: sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==} + '@eslint/object-schema@3.0.2': + resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/plugin-kit@0.6.0': @@ -431,14 +431,6 @@ packages: cpu: [x64] os: [win32] - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.1': - resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -573,14 +565,14 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/lodash@4.17.23': - resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - '@types/node@20.19.33': - resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + '@types/node@20.19.35': + resolution: {integrity: sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==} - '@types/node@25.2.3': - resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/node@25.3.3': + resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -606,63 +598,63 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.55.0': - resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.55.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.55.0': - resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.55.0': - resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.55.0': - resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.55.0': - resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.55.0': - resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.55.0': - resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.55.0': - resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.55.0': - resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.55.0': - resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@wdio/config@9.23.2': @@ -726,6 +718,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -738,14 +735,14 @@ packages: ajv: optional: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - allure-commandline@2.36.0: - resolution: {integrity: sha512-ls/4fk2Psv2Tu2PbWFrQPmUnm3gmmO9MBan4MuPWwqdkJPEmln2KRwtvtWYr9Av+e5AnFK1fGXWVyxqJIPiPwA==} + allure-commandline@2.37.0: + resolution: {integrity: sha512-s3zZ8zjqo2U3i5Lb3iLOCjwWQCtGK58GVpScTnZddOpgTXBDXAbXn+pT7QXN4NiY7pho6xw+UgyREyCRnx/9ug==} hasBin: true allure-js-commons@3.4.5: @@ -756,6 +753,14 @@ packages: allure-playwright: optional: true + allure-js-commons@3.5.0: + resolution: {integrity: sha512-iBVFNQkX5i48QGlb5U3iWm+NiNOl/ucxv6dvEJBNeJTPMI8t0Dn0CuXMQEiv4forSSAppD7FB9uGal2JwunH/A==} + peerDependencies: + allure-playwright: 3.5.0 + peerDependenciesMeta: + allure-playwright: + optional: true + allure-playwright@3.4.5: resolution: {integrity: sha512-pVewTpU9Z4qgT14VJdtYLAfF8rWROuESmvDkvyu/QnFWhRFrcDBnomynj84yx/QpXyMjJL+qu1yMU2z4Mq1YnA==} peerDependencies: @@ -777,64 +782,64 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - appium-adb@14.2.0: - resolution: {integrity: sha512-gT3eg+ZIG+xnNrmVja5BQy0yZLILlJnkF4pFwOgoPKf3e77fBRAo8CdzaYs2/oXs5YY7Tzx3w5ASvaHi1yAPmA==} + appium-adb@14.3.0: + resolution: {integrity: sha512-S1ZKK3R/nRlTMML+G5QliomDtbIYOxna6jOfJeX6X1fvN5Kg4dJo8GQW0+4Y6zHTA5cURWGiDpju+L7ohmJj6Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-android-driver@12.6.6: - resolution: {integrity: sha512-P4qkey5RbiGuuvi0ufS/GizX6MntdessACoqbgL7ieHufnQYkCY3fTHQM8JoIvqFvjFe95UMvjHyHuHJqshDqA==} + appium-android-driver@13.0.0: + resolution: {integrity: sha512-+q7+jPthCLFr4fYQeYV6eKQv4giKj6Pp1Y7qBI87B2+mHn0JgIDZXIyH9Xgt14e0Mj/sYOJN8VS/xPojA9Iang==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} peerDependencies: appium: ^3.0.0-rc.2 - appium-chromedriver@8.2.6: - resolution: {integrity: sha512-WYZ/QYbMy7rPOLNHAhML36/3IjXbyHX2ksWnbq0p9i3dMRUlJEfbYMFfImSleoPFEY3yNXwmDfgGM9rifr2liA==} + appium-chromedriver@8.2.14: + resolution: {integrity: sha512-yTDF+OjsgHdsTRTl/AJsAwwTGdAT41rBaj/+S5IrLTkY2gISdI2A+z6AUeN/gWVXit6y/pGQnlwW5+QqfRPCLg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-idb@2.0.8: - resolution: {integrity: sha512-SlJ3c8XcSKFnGydDr7CFKX7gxsWl/yW65/GO6ql2pIk56XjRLpv5Z4+RZQMzGp2GtOaw+KDRyjTezIIzcGWOOw==} + appium-idb@2.0.9: + resolution: {integrity: sha512-K1puvoS7VjkDhUSr9RDrXt7hZ6+JNBPEjthAohHdhdDHHk4NCBNfb6wGdcX20p/E//8x/kLnydNAzTDQdn3VGg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-ios-device@3.1.9: - resolution: {integrity: sha512-j4zNwszDvBHqyZKqX99RLcif3CnuYDpVKA9E2DBM/4mOFYT+o3bPAMrPZRx2Tt3RImoTCUrTCUinYopsPqX2Eg==} + appium-ios-device@3.1.10: + resolution: {integrity: sha512-2oE7yQtLSdrcZ9YArqgGguzDuiplHj0GXSMlTfwTXl0n22DEzkV0M1mXdaNaWNuzVBJ5VDc1EuYv38p1ruuk2g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-ios-remotexpc@0.29.0: - resolution: {integrity: sha512-TrVaAdLiyEl5dviFGGaPH1Vi+bXWgXUFrBCQ7isRnS6ukyPAyGxpR81im/V5MKc4TYuU2w3gg+gdV4zQ/a98Jg==} + appium-ios-remotexpc@0.30.0: + resolution: {integrity: sha512-I4CPI+U5wvzAa2P92CGtdP5ZClauebYMYvAc7Sx45Nw1JhygpqksQ9y3sJbB1PR8tqyWQYIqEoHSoe7UaaUT6g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-ios-simulator@8.0.11: - resolution: {integrity: sha512-DMm+XyS4o4iNPJwcGI7eecfmH6B7E8Q/1pANjWIOQhac3hOdUdo1KZxbnIQ4lUbSLzvLWL1T5Rts1UP//Jtwtg==} + appium-ios-simulator@8.0.12: + resolution: {integrity: sha512-ZIq9k0PJTq7MtttQmu8pBkQE7i1TNw/itqklAkwmstld8vTAn2RXv3+hAwFo1aZt01gdSPjky5EFgizNu72//Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} appium-ios-tuntap@0.1.3: resolution: {integrity: sha512-UZYWTIQrdKU3nwL9YjlQG19LWIzTs0CeG1FdgZXUZRu699z7rTJJu/d6JOuHNf/akj3BXRmbItOvllrDqEIz6A==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-remote-debugger@15.3.4: - resolution: {integrity: sha512-BZlNJ7qHL17JJame18JEKQDgfP7vFjViPZNCifIKRQU4KRaCQmEV4I5S+0TpBUFm0dG44cbWOp1Sfui7XyxzVw==} + appium-remote-debugger@15.5.0: + resolution: {integrity: sha512-2b2E/O00IDLvYIqdWA36qYCMRuS6i+BLZGB6A5SYvvc4DcDUBHP4SKgy+k+LLxj8xOfEFyawtwYd207ljLxMMg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-uiautomator2-driver@6.8.0: - resolution: {integrity: sha512-41xUQ04qs6SW3r9bYoqdAZyFHTBbVnfnEzFvsPCCeeZSzQwlQToO+u0hrdgcudFR5VZWwTvl07EIH4mWqrlhsg==} + appium-uiautomator2-driver@7.0.0: + resolution: {integrity: sha512-ct+X87CVbKkXTEzl/LG4WXpw26U5jT50cFTCQ6NxnSbJi4JX5Trn1EMsSdO0OxAL3qg3rk9zL7j1JBnIzj+KBQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} peerDependencies: appium: ^3.0.0-rc.2 - appium-uiautomator2-server@9.11.0: - resolution: {integrity: sha512-D5v67oJ75WGZ7eYffiWwLnRxqdFVSavIU6QPDxxl3mLvgkic4vksJ1i6xh+l98yyKpz+nSJbxu6oxbrP9IM0hw==} + appium-uiautomator2-server@9.11.1: + resolution: {integrity: sha512-MAlnHFhUdQ/gdpzXcJlK5chuMQLjhOqeoD1gPGmsr3raAElPHdKDYzSiaxqvDFu7XRYug6JfWmBsTSfdASy/RQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-webdriveragent@11.1.4: - resolution: {integrity: sha512-5NmVn2Qi7jezSXEHAOLt3E5qnp/9Mv9v1nzdbvjn8YN0O15pACEz3eqN1SBpjQ8A3pR8jBW7h4olJLoTs/+Y+A==} + appium-webdriveragent@11.1.6: + resolution: {integrity: sha512-7Ga9qqfZWtCXa+G50TFhpH8cMzA07yNtBDTZgfJehYFGzO4qx35sabzNGQ4z6GkxvuUvbHXvu8KTFlVNhgXR3w==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-xcode@6.1.8: - resolution: {integrity: sha512-nk86u0wo4ZPCxQsiF/1PR/HewpB5NxSkNxUttRLiWEbMTA8FPqDlbUfrD/xaywMYUYkYIk+W9ufz3Gg0CKMBAA==} + appium-xcode@6.1.9: + resolution: {integrity: sha512-m7bQPXMUitycAvPNmNQ/UdoZJhtcH2zCjxXcvQYi4uZTHqexcjy76MpMrVFsESJ7Qd8+0U2vmnMNpfB/M/BupQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - appium-xcuitest-driver@10.21.2: - resolution: {integrity: sha512-CodjJD+bQC4oyHmbyuIyN9+bGpzBy+S2MmX7P/GL0AV0FvWYwNde+4wxdX421O14s8q94fHIPNeX+4AsEdYnng==} + appium-xcuitest-driver@10.24.1: + resolution: {integrity: sha512-f2Ml3LrQFOGmumUle8DJQwVHlIy1XFXK8H1HRozZfFV0YFG3KQypO3XtMNhWuPtp5q6Mja6cOhUiHLcrtQy5jw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} peerDependencies: appium: ^3.0.0-rc.2 @@ -893,11 +898,8 @@ packages: axios@1.13.3: resolution: {integrity: sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==} - axios@1.13.4: - resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} - - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} b4a@1.7.3: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} @@ -910,6 +912,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -961,6 +967,7 @@ packages: basic-ftp@5.1.0: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} @@ -989,6 +996,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1294,8 +1305,8 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@17.2.4: - resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -1397,30 +1408,26 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-plugin-perfectionist@5.5.0: - resolution: {integrity: sha512-lZX2KUpwOQf7J27gAg/6vt8ugdPULOLmelM8oDJPMbaN7P2zNNeyS9yxGSmJcKX0SF9qR/962l9RWM2Z5jpPzg==} + eslint-plugin-perfectionist@5.6.0: + resolution: {integrity: sha512-pxrLrfRp5wl1Vol1fAEa/G5yTXxefTPJjz07qC7a8iWFXcOZNuWBItMQ2OtTzfQIvMq6bMyYcrzc3Wz++na55Q==} engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: - eslint: '>=8.45.0' + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 - eslint-scope@9.1.0: - resolution: {integrity: sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==} + eslint-scope@9.1.1: + resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@5.0.0: - resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.0.0: - resolution: {integrity: sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==} + eslint@10.0.2: + resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1429,8 +1436,8 @@ packages: jiti: optional: true - espree@11.1.0: - resolution: {integrity: sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==} + espree@11.1.1: + resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} esprima@4.0.1: @@ -1680,12 +1687,12 @@ packages: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} - glob@13.0.2: - resolution: {integrity: sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==} - engines: {node: 20 || >=22} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} - globals@17.3.0: - resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} globby@11.1.0: @@ -1792,8 +1799,8 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} - io.appium.settings@7.0.18: - resolution: {integrity: sha512-1JJcSRtvTZGlX8NQG+2Zf/0qZ6EQSIioeiUfnhy6uUej8MlybAhXR6rNJw5TAvMr4oX06vNyHD/+YvFHWg0Hhw==} + io.appium.settings@7.0.20: + resolution: {integrity: sha512-s/IWqO8oWwoZYKsgUAaZWOjFm2WspLSTrtY0H7M/+DS1EHJi8qNW3NMdb0SIjwGfVB4OdmvIONWOivRV2hT/2w==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} ip-address@10.1.0: @@ -2080,20 +2087,20 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.1.2: - resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} - engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} mitt@3.0.1: @@ -2144,8 +2151,8 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - node-addon-api@8.5.0: - resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + node-addon-api@8.6.0: + resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} engines: {node: ^18 || ^20 || >= 21} node-devicectl@1.1.4: @@ -2161,8 +2168,8 @@ packages: encoding: optional: true - node-simctl@8.1.5: - resolution: {integrity: sha512-8lQlne56cXGpPHjv49QXLQSOJuH+onlxHemlguSsutwbSdW+/ChC+xX932BEoG3qx62fpMPzRj3v2I1wVT4Ezw==} + node-simctl@8.1.6: + resolution: {integrity: sha512-SSwNzq4Tl575EaVFCIotDvDDV5XYR7676aN78lv/fhdxOQ+ZM6QZdIa/ZTXiDMc/Jd3wQ4L24E0d4Cqb6jy+Ew==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} normalize-package-data@8.0.0: @@ -2307,9 +2314,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -2485,8 +2492,8 @@ packages: rgb2hex@0.2.5: resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} - rimraf@6.1.2: - resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} hasBin: true @@ -2731,12 +2738,12 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - teen_process@4.0.8: - resolution: {integrity: sha512-0DTX2KfgVOr6+8TVmheEdiJHZ/bPOPeJuX0yvv5VOX3x+OFteNkmWkI+hX6zTkzxjddrktsrXkacfS2Gom1YyA==} + teen_process@4.0.10: + resolution: {integrity: sha512-xEQ0UCeUoprhDDADFKaxv9nzE+PlDTw/mgG0aX7ccxg+EGx8bCEiX25qQ0JPSjSS66sQyXEPFQR3nV5ZxiOcmw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} - teen_process@4.0.9: - resolution: {integrity: sha512-AdH4nuHQTTiFEnib3wWnepnfa7Vz8QzOZ7EsLM8iz8pOlZmshjnODmWTt/8OA6v6A9gACURKc0OddGX28UoxFQ==} + teen_process@4.0.8: + resolution: {integrity: sha512-0DTX2KfgVOr6+8TVmheEdiJHZ/bPOPeJuX0yvv5VOX3x+OFteNkmWkI+hX6zTkzxjddrktsrXkacfS2Gom1YyA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: '>=10'} text-decoder@1.2.3: @@ -2825,11 +2832,11 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript-eslint@8.55.0: - resolution: {integrity: sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==} + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' typescript@5.9.3: @@ -2843,15 +2850,15 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} undici@6.23.0: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} - undici@7.21.0: - resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} unicorn-magic@0.3.0: @@ -3250,18 +3257,18 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.2)': dependencies: - eslint: 10.0.0 + eslint: 10.0.2 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.1': + '@eslint/config-array@0.23.2': dependencies: - '@eslint/object-schema': 3.0.1 + '@eslint/object-schema': 3.0.2 debug: 4.4.3 - minimatch: 10.1.2 + minimatch: 10.2.4 transitivePeerDependencies: - supports-color @@ -3273,11 +3280,11 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.0.0)': + '@eslint/js@10.0.1(eslint@10.0.2)': optionalDependencies: - eslint: 10.0.0 + eslint: 10.0.2 - '@eslint/object-schema@3.0.1': {} + '@eslint/object-schema@3.0.2': {} '@eslint/plugin-kit@0.6.0': dependencies: @@ -3391,12 +3398,6 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.1': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3549,7 +3550,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 25.2.3 + '@types/node': 25.3.3 '@types/gh-pages@6.1.0': {} @@ -3557,17 +3558,17 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.3 - '@types/lodash@4.17.23': {} + '@types/lodash@4.17.24': {} - '@types/node@20.19.33': + '@types/node@20.19.35': dependencies: undici-types: 6.21.0 - '@types/node@25.2.3': + '@types/node@25.3.3': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 '@types/normalize-package-data@2.4.4': {} @@ -3585,22 +3586,22 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.3 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.2.3 + '@types/node': 25.3.3 optional: true - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2)(typescript@5.9.3))(eslint@10.0.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 10.0.0 + '@typescript-eslint/parser': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 10.0.2 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -3608,58 +3609,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@10.0.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - eslint: 10.0.0 + eslint: 10.0.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.55.0': + '@typescript-eslint/scope-manager@8.56.1': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 - '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@10.0.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.2)(typescript@5.9.3) debug: 4.4.3 - eslint: 10.0.0 + eslint: 10.0.2 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.55.0': {} + '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - minimatch: 9.0.5 + minimatch: 10.2.4 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -3667,21 +3668,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@10.0.2)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 10.0.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 10.0.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.55.0': + '@typescript-eslint/visitor-keys@8.56.1': dependencies: - '@typescript-eslint/types': 8.55.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 '@wdio/config@9.23.2': dependencies: @@ -3709,15 +3710,15 @@ snapshots: '@wdio/repl@9.16.2': dependencies: - '@types/node': 20.19.33 + '@types/node': 20.19.35 '@wdio/types@9.23.2': dependencies: - '@types/node': 20.19.33 + '@types/node': 20.19.35 '@wdio/types@9.24.0': dependencies: - '@types/node': 20.19.33 + '@types/node': 20.19.35 '@wdio/utils@9.23.2': dependencies: @@ -3757,9 +3758,9 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-walk@8.3.4: dependencies: @@ -3767,13 +3768,15 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -3787,7 +3790,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - allure-commandline@2.36.0: {} + allure-commandline@2.37.0: {} allure-js-commons@3.4.5(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)): dependencies: @@ -3795,6 +3798,12 @@ snapshots: optionalDependencies: allure-playwright: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) + allure-js-commons@3.5.0(allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2)): + dependencies: + md5: 2.3.0 + optionalDependencies: + allure-playwright: 3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2) + allure-playwright@3.4.5(patch_hash=8445e5b25f59024b1c2c8950f356f63676ad3ea2a153b051a41eff8ca5f3b305)(@playwright/test@1.58.2): dependencies: '@playwright/test': 1.58.2 @@ -3810,7 +3819,7 @@ snapshots: ansi-styles@6.2.3: {} - appium-adb@14.2.0: + appium-adb@14.3.0: dependencies: '@appium/support': 7.0.5 async-lock: 1.4.1 @@ -3820,30 +3829,30 @@ snapshots: lodash: 4.17.23 lru-cache: 11.2.6 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - appium-android-driver@12.6.6(appium@3.2.0): + appium-android-driver@13.0.0(appium@3.2.0): dependencies: '@appium/support': 7.0.5 '@colors/colors': 1.6.0 appium: 3.2.0 - appium-adb: 14.2.0 - appium-chromedriver: 8.2.6 + appium-adb: 14.3.0 + appium-chromedriver: 8.2.14 asyncbox: 6.1.0 - axios: 1.13.4 + axios: 1.13.6 bluebird: 3.7.2 - io.appium.settings: 7.0.18 + io.appium.settings: 7.0.20 lodash: 4.17.23 lru-cache: 11.2.6 moment: 2.30.1 moment-timezone: 0.6.0 portscanner: 2.2.0 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 ws: 8.19.0 transitivePeerDependencies: - bare-abort-controller @@ -3853,19 +3862,19 @@ snapshots: - supports-color - utf-8-validate - appium-chromedriver@8.2.6: + appium-chromedriver@8.2.14: dependencies: '@appium/base-driver': 10.2.0 '@appium/support': 7.0.5 '@xmldom/xmldom': 0.8.11 - appium-adb: 14.2.0 + appium-adb: 14.3.0 asyncbox: 6.1.0 - axios: 1.13.4 + axios: 1.13.6 bluebird: 3.7.2 compare-versions: 6.1.1 lodash: 4.17.23 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 xpath: 0.0.34 transitivePeerDependencies: - bare-abort-controller @@ -3873,23 +3882,23 @@ snapshots: - react-native-b4a - supports-color - appium-idb@2.0.8: + appium-idb@2.0.9: dependencies: '@appium/support': 7.0.5 asyncbox: 6.1.0 bluebird: 3.7.2 lodash: 4.17.23 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - appium-ios-device@3.1.9: + appium-ios-device@3.1.10: dependencies: '@appium/support': 7.0.5 asyncbox: 6.1.0 - axios: 1.13.5 + axios: 1.13.6 bluebird: 3.7.2 bplist-creator: 0.1.1 bplist-parser: 0.3.2 @@ -3900,15 +3909,15 @@ snapshots: - debug - react-native-b4a - appium-ios-remotexpc@0.29.0: + appium-ios-remotexpc@0.30.0: dependencies: '@appium/strongbox': 1.0.1 '@appium/support': 7.0.5 - '@types/node': 25.2.3 + '@types/node': 25.3.3 '@xmldom/xmldom': 0.9.8 appium-ios-tuntap: 0.1.3 - axios: 1.13.5 - minimatch: 10.1.2 + axios: 1.13.6 + minimatch: 10.2.4 npm-run-all2: 8.0.4 transitivePeerDependencies: - bare-abort-controller @@ -3916,18 +3925,18 @@ snapshots: - react-native-b4a optional: true - appium-ios-simulator@8.0.11: + appium-ios-simulator@8.0.12: dependencies: '@appium/support': 7.0.5 '@xmldom/xmldom': 0.8.11 - appium-xcode: 6.1.8 + appium-xcode: 6.1.9 async-lock: 1.4.1 asyncbox: 6.1.0 bluebird: 3.7.2 lodash: 4.17.23 - node-simctl: 8.1.5 + node-simctl: 8.1.6 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug @@ -3936,7 +3945,7 @@ snapshots: appium-ios-tuntap@0.1.3: dependencies: '@appium/support': 7.0.5 - node-addon-api: 8.5.0 + node-addon-api: 8.6.0 typescript: 5.9.3 transitivePeerDependencies: - bare-abort-controller @@ -3944,37 +3953,37 @@ snapshots: - react-native-b4a optional: true - appium-remote-debugger@15.3.4: + appium-remote-debugger@15.5.0: dependencies: '@appium/base-driver': 10.2.0 '@appium/support': 7.0.5 - appium-ios-device: 3.1.9 + appium-ios-device: 3.1.10 async-lock: 1.4.1 asyncbox: 6.1.0 bluebird: 3.7.2 - glob: 13.0.2 + glob: 13.0.6 lodash: 4.17.23 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - supports-color - appium-uiautomator2-driver@6.8.0(patch_hash=8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121)(appium@3.2.0): + appium-uiautomator2-driver@7.0.0(patch_hash=8226be3d8d63cd3e3963f8450fc068a726a9a71eddecad1a612f92bdbd92d121)(appium@3.2.0): dependencies: appium: 3.2.0 - appium-adb: 14.2.0 - appium-android-driver: 12.6.6(appium@3.2.0) - appium-uiautomator2-server: 9.11.0 + appium-adb: 14.3.0 + appium-android-driver: 13.0.0(appium@3.2.0) + appium-uiautomator2-server: 9.11.1 asyncbox: 6.1.0 - axios: 1.13.4 + axios: 1.13.6 bluebird: 3.7.2 css-selector-parser: 3.3.0 - io.appium.settings: 7.0.18 + io.appium.settings: 7.0.20 lodash: 4.17.23 portscanner: 2.2.0 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - bufferutil @@ -3983,28 +3992,28 @@ snapshots: - supports-color - utf-8-validate - appium-uiautomator2-server@9.11.0: {} + appium-uiautomator2-server@9.11.1: {} - appium-webdriveragent@11.1.4: + appium-webdriveragent@11.1.6: dependencies: '@appium/base-driver': 10.2.0 '@appium/strongbox': 1.0.1 '@appium/support': 7.0.5 - appium-ios-device: 3.1.9 - appium-ios-simulator: 8.0.11 + appium-ios-device: 3.1.10 + appium-ios-simulator: 8.0.12 async-lock: 1.4.1 asyncbox: 6.1.0 - axios: 1.13.5 + axios: 1.13.6 bluebird: 3.7.2 lodash: 4.17.23 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - supports-color - appium-xcode@6.1.8: + appium-xcode@6.1.9: dependencies: '@appium/support': 7.0.5 asyncbox: 6.1.0 @@ -4012,23 +4021,23 @@ snapshots: lodash: 4.17.23 plist: 3.1.0 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 transitivePeerDependencies: - bare-abort-controller - debug - react-native-b4a - appium-xcuitest-driver@10.21.2(appium@3.2.0): + appium-xcuitest-driver@10.24.1(appium@3.2.0): dependencies: '@appium/strongbox': 1.0.1 '@colors/colors': 1.6.0 appium: 3.2.0 - appium-idb: 2.0.8 - appium-ios-device: 3.1.9 - appium-ios-simulator: 8.0.11 - appium-remote-debugger: 15.3.4 - appium-webdriveragent: 11.1.4 - appium-xcode: 6.1.8 + appium-idb: 2.0.9 + appium-ios-device: 3.1.10 + appium-ios-simulator: 8.0.12 + appium-remote-debugger: 15.5.0 + appium-webdriveragent: 11.1.6 + appium-xcode: 6.1.9 async-lock: 1.4.1 asyncbox: 6.1.0 bluebird: 3.7.2 @@ -4040,14 +4049,14 @@ snapshots: moment: 2.30.1 moment-timezone: 0.6.0 node-devicectl: 1.1.4 - node-simctl: 8.1.5 + node-simctl: 8.1.6 portscanner: 2.2.0 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 winston: 3.19.0 ws: 8.19.0 optionalDependencies: - appium-ios-remotexpc: 0.29.0 + appium-ios-remotexpc: 0.30.0 transitivePeerDependencies: - bare-abort-controller - bufferutil @@ -4151,15 +4160,7 @@ snapshots: transitivePeerDependencies: - debug - axios@1.13.4: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - axios@1.13.5: + axios@1.13.6: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 @@ -4171,6 +4172,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.8.2: {} bare-fs@4.5.3: @@ -4256,6 +4259,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4317,7 +4324,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.21.0 + undici: 7.22.0 whatwg-mimetype: 4.0.0 chromium-bidi@0.5.8(devtools-protocol@0.0.1232444): @@ -4529,7 +4536,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv@17.2.4: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: dependencies: @@ -4623,16 +4630,16 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-perfectionist@5.5.0(eslint@10.0.0)(typescript@5.9.3): + eslint-plugin-perfectionist@5.6.0(eslint@10.0.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - eslint: 10.0.0 + '@typescript-eslint/utils': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + eslint: 10.0.2 natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-scope@9.1.0: + eslint-scope@9.1.1: dependencies: '@types/esrecurse': 4.3.1 '@types/estree': 1.0.8 @@ -4641,15 +4648,13 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} - eslint-visitor-keys@5.0.0: {} - - eslint@10.0.0: + eslint@10.0.2: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.1 + '@eslint/config-array': 0.23.2 '@eslint/config-helpers': 0.5.2 '@eslint/core': 1.1.0 '@eslint/plugin-kit': 0.6.0 @@ -4657,13 +4662,13 @@ snapshots: '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 9.1.0 - eslint-visitor-keys: 5.0.0 - espree: 11.1.0 + eslint-scope: 9.1.1 + eslint-visitor-keys: 5.0.1 + espree: 11.1.1 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -4674,17 +4679,17 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.1.2 + minimatch: 10.2.4 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: - supports-color - espree@11.1.0: + espree@11.1.1: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 5.0.0 + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -4961,24 +4966,24 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 + minimatch: 9.0.9 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@13.0.0: dependencies: - minimatch: 10.1.2 - minipass: 7.1.2 - path-scurry: 2.0.1 + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 - glob@13.0.2: + glob@13.0.6: dependencies: - minimatch: 10.1.2 - minipass: 7.1.2 - path-scurry: 2.0.1 + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 - globals@17.3.0: {} + globals@17.4.0: {} globby@11.1.0: dependencies: @@ -5084,14 +5089,14 @@ snapshots: ini@6.0.0: {} - io.appium.settings@7.0.18: + io.appium.settings@7.0.20: dependencies: '@appium/logger': 2.0.4 asyncbox: 6.1.0 bluebird: 3.7.2 lodash: 4.17.23 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 ip-address@10.1.0: {} @@ -5341,19 +5346,19 @@ snapshots: minimalistic-assert@1.0.1: optional: true - minimatch@10.1.2: + minimatch@10.2.4: dependencies: - '@isaacs/brace-expansion': 5.0.1 + brace-expansion: 5.0.4 - minimatch@5.1.6: + minimatch@5.1.9: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: + minimatch@9.0.9: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} + minipass@7.1.3: {} mitt@3.0.1: {} @@ -5394,29 +5399,29 @@ snapshots: netmask@2.0.2: {} - node-addon-api@8.5.0: + node-addon-api@8.6.0: optional: true node-devicectl@1.1.4: dependencies: '@appium/logger': 2.0.4 lodash: 4.17.23 - teen_process: 4.0.9 + teen_process: 4.0.10 node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 optional: true - node-simctl@8.1.5: + node-simctl@8.1.6: dependencies: '@appium/logger': 2.0.4 asyncbox: 6.1.0 bluebird: 3.7.2 lodash: 4.17.23 - rimraf: 6.1.2 + rimraf: 6.1.3 semver: 7.7.4 - teen_process: 4.0.9 + teen_process: 4.0.10 uuid: 13.0.0 which: 6.0.1 @@ -5583,12 +5588,12 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 - path-scurry@2.0.1: + path-scurry@2.0.2: dependencies: lru-cache: 11.2.6 - minipass: 7.1.2 + minipass: 7.1.3 path-to-regexp@8.3.0: {} @@ -5650,7 +5655,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -5765,7 +5770,7 @@ snapshots: readdir-glob@1.1.3: dependencies: - minimatch: 5.1.6 + minimatch: 5.1.9 require-directory@2.1.1: {} @@ -5788,9 +5793,9 @@ snapshots: rgb2hex@0.2.5: {} - rimraf@6.1.2: + rimraf@6.1.3: dependencies: - glob: 13.0.2 + glob: 13.0.6 package-json-from-dist: 1.0.1 router@2.2.0: @@ -6116,12 +6121,12 @@ snapshots: - bare-abort-controller - react-native-b4a - teen_process@4.0.8: + teen_process@4.0.10: dependencies: lodash: 4.17.23 shell-quote: 1.8.3 - teen_process@4.0.9: + teen_process@4.0.8: dependencies: lodash: 4.17.23 shell-quote: 1.8.3 @@ -6164,14 +6169,14 @@ snapshots: dependencies: typescript: 5.9.3 - ts-node@10.9.2(@types/node@25.2.3)(typescript@5.9.3): + ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.2.3 + '@types/node': 25.3.3 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -6206,13 +6211,13 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@10.0.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - eslint: 10.0.0 + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.2)(typescript@5.9.3))(eslint@10.0.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.2)(typescript@5.9.3) + eslint: 10.0.2 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6227,11 +6232,11 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: {} + undici-types@7.18.2: {} undici@6.23.0: {} - undici@7.21.0: {} + undici@7.22.0: {} unicorn-magic@0.3.0: {} @@ -6286,7 +6291,7 @@ snapshots: webdriver@9.23.2: dependencies: - '@types/node': 20.19.33 + '@types/node': 20.19.35 '@types/ws': 8.18.1 '@wdio/config': 9.23.2 '@wdio/logger': 9.18.0 @@ -6307,7 +6312,7 @@ snapshots: webdriverio@9.23.2(puppeteer-core@21.11.0): dependencies: - '@types/node': 20.19.33 + '@types/node': 20.19.35 '@types/sinonjs__fake-timers': 8.1.5 '@wdio/config': 9.23.2 '@wdio/logger': 9.18.0 From 1807785784fade3e989fd468cf4b06a74083ba74 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 10:33:30 +1100 Subject: [PATCH 138/184] feat: workflow input for pro tests --- .github/workflows/android-regression.yml | 27 +++++++++++-------- .github/workflows/ios-regression.yml | 22 ++++++++------- run/test/specs/message_length.spec.ts | 1 + ...r_actions_animated_profile_picture.spec.ts | 4 +++ run/test/specs/user_actions_pin_unpin.spec.ts | 2 ++ run/types/sessionIt.ts | 6 +++-- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index bd9bc2088..cc83a853d 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -33,14 +33,17 @@ on: - 'low-risk' - '' + RUN_PRO_TESTS: + description: 'include Session Pro tests in this run' + required: false + default: false + type: boolean + ALLURE_ENABLED: description: 'generate allure report' required: false - default: 'true' - type: choice - options: - - 'true' - - 'false' + default: true + type: boolean PLAYWRIGHT_RETRIES_COUNT: description: 'retries of failing tests to do at most' @@ -74,7 +77,7 @@ jobs: BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CI: 1 - ALLURE_ENABLED: ${{ github.event.inputs.ALLURE_ENABLED}} + ALLURE_ENABLED: ${{ github.event.inputs.ALLURE_ENABLED }} IOS_APP_PATH_PREFIX: '' ANDROID_APK: './extracted/session-android.apk' APPIUM_ADB_FULL_PATH: '/opt/android/platform-tools/adb' @@ -85,6 +88,8 @@ jobs: PRINT_FAILED_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL != 'minimal' && '1' || '0' }} # Show stdout/stderr if test fails (@session-foundation/playwright-reporter/ flag) PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} + PRO_GREP_INVERT: ${{ inputs.RUN_PRO_TESTS != true && '--grep-invert @pro' || '' }} + PRO_INVERT_SUFFIX: ${{ inputs.RUN_PRO_TESTS != true && '|@pro' || '' }} steps: - uses: actions/checkout@v6 @@ -94,7 +99,7 @@ jobs: - name: Fetch result history from gh-pages uses: ./github/actions/fetch-allure-history - if: ${{ env.ALLURE_ENABLED == 'true' }} + if: ${{ env.ALLURE_ENABLED }} with: PLATFORM: ${{ env.PLATFORM }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -205,7 +210,7 @@ jobs: PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" $PRO_GREP_INVERT #Note: this has to be double quotes - name: Upload results of this run uses: ./github/actions/upload-test-results @@ -227,7 +232,7 @@ jobs: PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${DEVICES_PER_TEST_COUNT}-devices)(?=.*@${{ github.event.inputs.RISK }})" $PRO_GREP_INVERT #Note: this has to be double quotes - name: Upload results of this run uses: ./github/actions/upload-test-results @@ -249,11 +254,11 @@ jobs: PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" --grep-invert "@1-devices|@2-devices" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" --grep-invert "@1-devices|@2-devices${PRO_INVERT_SUFFIX}" #Note: this has to be double quotes - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report - if: ${{ always() && env.ALLURE_ENABLED == 'true' }} + if: ${{ always() && env.ALLURE_ENABLED }} with: PLATFORM: ${{ env.PLATFORM }} BUILD_NUMBER: ${{ env.BUILD_NUMBER }} diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 743649447..8e5373d27 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -24,14 +24,17 @@ on: - 'low-risk' - '' + RUN_PRO_TESTS: + description: 'include Session Pro tests in this run' + required: false + default: true + type: boolean + ALLURE_ENABLED: description: 'generate allure report' required: false - default: 'true' - type: choice - options: - - 'true' - - 'false' + default: true + type: boolean PLAYWRIGHT_RETRIES_COUNT: description: 'retries of failing tests to do at most' @@ -75,7 +78,7 @@ jobs: BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CI: 1 - ALLURE_ENABLED: ${{ github.event.inputs.ALLURE_ENABLED}} + ALLURE_ENABLED: ${{ github.event.inputs.ALLURE_ENABLED }} IOS_APP_PATH_PREFIX: './extracted/Session.app' ANDROID_APK: '' APPIUM_ADB_FULL_PATH: '' @@ -87,6 +90,7 @@ jobs: PRINT_ONGOING_TEST_LOGS: ${{ github.event.inputs.LOG_LEVEL == 'verbose' && '1' || '0' }} # Show everything as it happens (@session-foundation/playwright-reporter/ flag) PLAYWRIGHT_WORKERS_COUNT: 3 # for iOS, this is the max we can have on our self-hosted runner SOGS_ADMIN_SEED: ${{ secrets.SOGS_ADMIN_SEED }} + PRO_GREP_INVERT: ${{ inputs.RUN_PRO_TESTS != true && '--grep-invert @pro' || '' }} steps: - uses: actions/checkout@v6 @@ -96,7 +100,7 @@ jobs: - name: Fetch result history from gh-pages uses: ./github/actions/fetch-allure-history - if: ${{ env.ALLURE_ENABLED == 'true' }} + if: ${{ env.ALLURE_ENABLED }} with: PLATFORM: ${{ env.PLATFORM }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -140,11 +144,11 @@ jobs: PRINT_ONGOING_TEST_LOGS: ${{ env.PRINT_ONGOING_TEST_LOGS }} run: | pwd - npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" #Note: this has to be double quotes + npx playwright test --grep "(?=.*@${PLATFORM})(?=.*@${{ github.event.inputs.RISK }})" $PRO_GREP_INVERT #Note: this has to be double quotes - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report - if: ${{ always() && env.ALLURE_ENABLED == 'true' }} + if: ${{ always() && env.ALLURE_ENABLED }} with: PLATFORM: ${{ env.PLATFORM }} BUILD_NUMBER: ${{ env.BUILD_NUMBER }} diff --git a/run/test/specs/message_length.spec.ts b/run/test/specs/message_length.spec.ts index 5e8661bdc..d423ff9b0 100644 --- a/run/test/specs/message_length.spec.ts +++ b/run/test/specs/message_length.spec.ts @@ -81,6 +81,7 @@ for (const testCase of messageLengthTestCases) { title: `Message length limit (${testCase.length} chars ${proSuffix})`, risk: 'high', countOfDevicesNeeded: 1, + isPro: testCase.pro, allureSuites: { parent: 'Sending Messages', suite: 'Rules', diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 231ca7b99..695eeb04e 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -25,6 +25,7 @@ bothPlatformsIt({ risk: 'high', countOfDevicesNeeded: 1, testCb: nonProAnimatedDP, + isPro: true, allureSuites: { parent: 'User Actions', suite: 'Change Profile Picture', @@ -36,6 +37,7 @@ bothPlatformsIt({ risk: 'high', countOfDevicesNeeded: 1, testCb: proAnimatedDP, + isPro: true, allureSuites: { parent: 'User Actions', suite: 'Change Profile Picture', @@ -47,6 +49,7 @@ bothPlatformsIt({ risk: 'low', countOfDevicesNeeded: 1, testCb: proActivatedCTA, + isPro: true, allureSuites: { parent: 'Session Pro', }, @@ -57,6 +60,7 @@ bothPlatformsIt({ risk: 'high', countOfDevicesNeeded: 2, testCb: proAnimatedDPShows, + isPro: true, allureSuites: { parent: 'Session Pro', }, diff --git a/run/test/specs/user_actions_pin_unpin.spec.ts b/run/test/specs/user_actions_pin_unpin.spec.ts index a8414eac7..f76442682 100644 --- a/run/test/specs/user_actions_pin_unpin.spec.ts +++ b/run/test/specs/user_actions_pin_unpin.spec.ts @@ -31,6 +31,7 @@ bothPlatformsIt({ risk: 'high', testCb: nonProPinnedLimit, countOfDevicesNeeded: 1, + isPro: true, allureSuites: { parent: 'Session Pro', }, @@ -42,6 +43,7 @@ bothPlatformsIt({ risk: 'high', testCb: proPinnedLimit, countOfDevicesNeeded: 1, + isPro: true, allureSuites: { parent: 'Session Pro', }, diff --git a/run/types/sessionIt.ts b/run/types/sessionIt.ts index e329b16be..635e6deb5 100644 --- a/run/types/sessionIt.ts +++ b/run/types/sessionIt.ts @@ -1,4 +1,3 @@ -// run/types/sessionIt.ts - Clean version matching original pattern import { test, type TestInfo } from '@playwright/test'; import { omit } from 'lodash'; @@ -20,6 +19,7 @@ type MobileItArgs = { risk: TestRisk; testCb: (platform: SupportedPlatformsType, testInfo: TestInfo) => Promise; shouldSkip?: boolean; + isPro?: boolean; allureSuites?: AllureSuiteConfig; allureDescription?: string; allureLinks?: { @@ -43,12 +43,14 @@ function mobileIt({ testCb, title, shouldSkip = false, + isPro = false, countOfDevicesNeeded, allureSuites, allureDescription, allureLinks, }: MobileItArgs) { - const testName = `${title} @${platform} @${risk ?? 'default'}-risk @${countOfDevicesNeeded}-devices`; + const proTag = isPro ? ' @pro' : ''; + const testName = `${title} @${platform} @${risk ?? 'default'}-risk @${countOfDevicesNeeded}-devices${proTag}`; if (shouldSkip) { test.skip(testName, () => { From 4501ce86635f03824805060820f41b2ec4c7a420 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 10:37:00 +1100 Subject: [PATCH 139/184] fix: check allure true --- .github/workflows/android-regression.yml | 4 ++-- .github/workflows/ios-regression.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index cc83a853d..fb077b7d7 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -99,7 +99,7 @@ jobs: - name: Fetch result history from gh-pages uses: ./github/actions/fetch-allure-history - if: ${{ env.ALLURE_ENABLED }} + if: ${{ env.ALLURE_ENABLED == 'true' }} with: PLATFORM: ${{ env.PLATFORM }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -258,7 +258,7 @@ jobs: - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report - if: ${{ always() && env.ALLURE_ENABLED }} + if: ${{ always() && env.ALLURE_ENABLED == 'true' }} with: PLATFORM: ${{ env.PLATFORM }} BUILD_NUMBER: ${{ env.BUILD_NUMBER }} diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 8e5373d27..b1638ef82 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -100,7 +100,7 @@ jobs: - name: Fetch result history from gh-pages uses: ./github/actions/fetch-allure-history - if: ${{ env.ALLURE_ENABLED }} + if: ${{ env.ALLURE_ENABLED == 'true' }} with: PLATFORM: ${{ env.PLATFORM }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -148,7 +148,7 @@ jobs: - name: Generate and publish test report uses: ./github/actions/generate-publish-test-report - if: ${{ always() && env.ALLURE_ENABLED }} + if: ${{ always() && env.ALLURE_ENABLED == 'true' }} with: PLATFORM: ${{ env.PLATFORM }} BUILD_NUMBER: ${{ env.BUILD_NUMBER }} From 54b7553dfa1e836a9ec1b1812b34cc4c8a220ad5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 10:44:11 +1100 Subject: [PATCH 140/184] fix: thread grep invert through to list tests action --- .github/workflows/android-regression.yml | 3 ++- .github/workflows/ios-regression.yml | 1 + github/actions/list-tests/action.yml | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index fb077b7d7..092b0b4a5 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -36,7 +36,7 @@ on: RUN_PRO_TESTS: description: 'include Session Pro tests in this run' required: false - default: false + default: true type: boolean ALLURE_ENABLED: @@ -198,6 +198,7 @@ jobs: with: PLATFORM: ${{ env.PLATFORM }} RISK: ${{ github.event.inputs.RISK }} + PRO_GREP_INVERT: ${{ env.PRO_GREP_INVERT }} - name: Run the 1-devices tests ​​with 4 workers continue-on-error: true diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index b1638ef82..734df4e92 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -135,6 +135,7 @@ jobs: with: PLATFORM: ${{ env.PLATFORM }} RISK: ${{ github.event.inputs.RISK }} + PRO_GREP_INVERT: ${{ env.PRO_GREP_INVERT }} - name: Run the iOS tests​​ (all device counts) env: diff --git a/github/actions/list-tests/action.yml b/github/actions/list-tests/action.yml index 5afd9fe49..7d666c6a2 100644 --- a/github/actions/list-tests/action.yml +++ b/github/actions/list-tests/action.yml @@ -10,6 +10,10 @@ inputs: RISK: description: "Risk level to filter tests 'high-risk'|'medium-risk'|'low-risk'|''" required: false + PRO_GREP_INVERT: + description: 'Optional --grep-invert flag to exclude @pro tests' + required: false + default: '' runs: using: 'composite' @@ -18,4 +22,4 @@ runs: shell: bash run: | pwd - npx playwright test --list --reporter list --grep "(?=.*@${{ inputs.PLATFORM }})(?=.*@${{ inputs.RISK }})" + npx playwright test --list --reporter list --grep "(?=.*@${{ inputs.PLATFORM }})(?=.*@${{ inputs.RISK }})" ${{ inputs.PRO_GREP_INVERT }} From 30bdaf666f7f8415f0e5651b1682c78b0ecfbe84 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 12:06:56 +1100 Subject: [PATCH 141/184] fix: assert() handles async matchers and chained properties --- run/test/utils/utilities.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index 25625d322..2508ce617 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -174,11 +174,19 @@ export function assert(actual: T, message: string) { return new Proxy(obj, { get(target, prop: string | symbol) { const val = Reflect.get(target, prop, target); - if (prop === 'not') return wrapMatchers(val as typeof matchers); + if (prop === 'not' || prop === 'resolves' || prop === 'rejects') + return wrapMatchers(val as typeof matchers); if (typeof val === 'function') { return (...args: unknown[]) => { try { - return (val as (...a: unknown[]) => unknown).apply(target, args); + const result = (val as (...a: unknown[]) => unknown).apply(target, args); + if (result instanceof Promise) { + return result.catch(() => { + console.log(`${message}\n actual: `, actual, '\n expected:', args[0]); + throw new Error(message); + }); + } + return result; } catch { console.log(`${message}\n actual: `, actual, '\n expected:', args[0]); throw new Error(message); From 01c361be320e47df50207ef0cc7dc2f838404b4b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 13:24:11 +1100 Subject: [PATCH 142/184] chore: copilot comments --- run/test/specs/linked_group_leave.spec.ts | 2 +- run/test/specs/recovery_banner.spec.ts | 6 +++--- run/test/utils/community.ts | 5 +++-- run/test/utils/device_registry.ts | 3 ++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/run/test/specs/linked_group_leave.spec.ts b/run/test/specs/linked_group_leave.spec.ts index fdf3ae3f1..47f06e318 100644 --- a/run/test/specs/linked_group_leave.spec.ts +++ b/run/test/specs/linked_group_leave.spec.ts @@ -34,7 +34,7 @@ async function leaveGroupLinkedDevice(platform: SupportedPlatformsType, testInfo // Create group with user A, user B and User C await createGroup(platform, device1, alice, device2, bob, device3, charlie, testGroupName); // If we know group is present on device4, we can check for just disappearance later (vs. hasElementBeenDeleted) - await device4.waitForTextElementToBePresent(new ConversationItem(device2, testGroupName)); + await device4.waitForTextElementToBePresent(new ConversationItem(device4, testGroupName)); // Leave Group on device 3 await device3.clickOnElementAll(new ConversationSettings(device3)); await device3.clickOnElementAll(new LeaveGroupMenuItem(device3)); diff --git a/run/test/specs/recovery_banner.spec.ts b/run/test/specs/recovery_banner.spec.ts index 5cca2fa5e..898bcac22 100644 --- a/run/test/specs/recovery_banner.spec.ts +++ b/run/test/specs/recovery_banner.spec.ts @@ -49,7 +49,7 @@ androidIt({ 'Verifies that the recovery password banner does not disappear if the conversation count drops below 3', }); -async function bannerShouldNotshow(device: DeviceWrapper) { +async function bannerShouldNotShow(device: DeviceWrapper) { await device.waitForTextElementToBePresent(new PlusButton(device)); await device.verifyElementNotPresent(new RevealRecoveryPhraseButton(device)); device.log('On home screen, banner did not appear'); @@ -69,7 +69,7 @@ async function bannerShowsThreeConvos(platform: SupportedPlatformsType, testInfo }); await test.step('Create three conversations, verify banner only appears after the third', async () => { for (const community of Object.values(communities).slice(0, 3)) { - await bannerShouldNotshow(device); + await bannerShouldNotShow(device); await joinCommunity(device, community.link, community.name); await device.navigateBack(); } @@ -92,7 +92,7 @@ async function bannerDisappearsAfterOpened(platform: SupportedPlatformsType, tes await device.clickOnElementAll(new RevealRecoveryPhraseButton(device)); await device.waitForTextElementToBePresent(new RecoveryPhraseContainer(device)); await device.navigateBack(); - await bannerShouldNotshow(device); + await bannerShouldNotShow(device); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index c48302e3c..f9f539c2b 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -32,8 +32,9 @@ export const joinCommunity = async ( export const joinCommunities = async (device: DeviceWrapper, number: number) => { const available = Object.values(communities).length; if (number > available) { - throw new Error(`joinCommunities: requested ${number} but only ${available} communities have been recorded - Check run/constants/community.ts for more`); + throw new Error( + `joinCommunities: requested ${number} but only ${available} communities have been recorded.\nCheck run/constants/community.ts for more` + ); } for (const community of Object.values(communities).slice(0, number)) { await joinCommunity(device, community.link, community.name); diff --git a/run/test/utils/device_registry.ts b/run/test/utils/device_registry.ts index 6e29ba6b7..5d8ba7f49 100644 --- a/run/test/utils/device_registry.ts +++ b/run/test/utils/device_registry.ts @@ -1,9 +1,10 @@ import type { TestInfo } from '@playwright/test'; +import type { SupportedPlatformsType } from './open_app'; + import { DeviceWrapper } from '../../types/DeviceWrapper'; import { getAdbFullPath } from './binaries'; import { androidAppPackage } from './capabilities_android'; -import { SupportedPlatformsType } from './open_app'; import { runScriptAndLog } from './utilities'; export type LogContext = { From 787bdc9b67defa8536778909c7804c3eb3633c3c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 16:02:57 +1100 Subject: [PATCH 143/184] chore: remove comment --- run/test/locators/global.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/run/test/locators/global.ts b/run/test/locators/global.ts index 58d264af4..19fabccc0 100644 --- a/run/test/locators/global.ts +++ b/run/test/locators/global.ts @@ -100,8 +100,6 @@ export class CopyURLButton extends LocatorsInterface { } } -// NOTE: iOS Pro CTAs use accessibility IDs, Donate CTA requires XPath fallback (see DeviceWrapper) -// See SES-4930 export class CTABody extends LocatorsInterface { public build() { switch (this.platform) { From 0c7db3079f8b7445c9f3e0c35a37887501d60ee5 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 2 Mar 2026 16:37:01 +1100 Subject: [PATCH 144/184] fix: use correct control message --- run/test/specs/group_tests_add_contact.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/test/specs/group_tests_add_contact.spec.ts b/run/test/specs/group_tests_add_contact.spec.ts index c5c4cf09e..7b803bb57 100644 --- a/run/test/specs/group_tests_add_contact.spec.ts +++ b/run/test/specs/group_tests_add_contact.spec.ts @@ -102,7 +102,7 @@ async function addContactToGroupHistory(platform: SupportedPlatformsType, testIn await unknown1.navigateBack(); await unknown1.clickOnElementAll(new ConversationItem(unknown1, group.groupName)); // Check for control message on device 4 await unknown1.waitForTextElementToBePresent(new MessageBody(unknown1, historicMsg)); - await unknown1.waitForControlMessageToBePresent(tStripped('groupInviteYou')); + await unknown1.waitForControlMessageToBePresent(tStripped('groupInviteYouHistory')); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(alice1, bob1, charlie1, unknown1); From 2e8ac0ae88adee8166a2a13147cf2a3125f33b31 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Mar 2026 13:52:09 +1100 Subject: [PATCH 145/184] feat: virtual camera image injection --- .github/workflows/android-regression.yml | 3 ++ package.json | 1 + run/test/utils/capabilities_android.ts | 8 ++-- run/types/DeviceWrapper.ts | 11 ++++++ scripts/resources/Toren1BD.posters | 11 ++++++ scripts/resources/placeholder.png | 3 ++ scripts/setup_virtual_scene.ts | 48 ++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 scripts/resources/Toren1BD.posters create mode 100644 scripts/resources/placeholder.png create mode 100644 scripts/setup_virtual_scene.ts diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 092b0b4a5..11d0f98f8 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -186,6 +186,9 @@ jobs: adb kill-server; adb start-server; + - name: Apply emulator virtual scene config + run: pnpm setup-virtual-scene + - name: Start emulators from snapshot shell: bash run: | diff --git a/package.json b/package.json index 055fac0d3..64a881793 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "cleanup-simulators": "npx ts-node scripts/cleanup_ios_simulators.ts", "create-simulators": "pnpm cleanup-simulators && npx ts-node scripts/create_ios_simulators.ts", "recover-emulators": "npx ts-node scripts/emulator_health.ts", + "setup-virtual-scene": "npx ts-node scripts/setup_virtual_scene.ts", "lint": "pnpm prettier . --write --cache && pnpm eslint . --cache ", "lint-check": "pnpm prettier . --check && pnpm eslint .", "tsc": "tsc", diff --git a/run/test/utils/capabilities_android.ts b/run/test/utils/capabilities_android.ts index e7aa4807f..8fbd07688 100644 --- a/run/test/utils/capabilities_android.ts +++ b/run/test/utils/capabilities_android.ts @@ -1,4 +1,3 @@ -import { AppiumAndroidCapabilities, AppiumCapabilities } from '@wdio/types/build/Capabilities'; import { W3CUiautomator2DriverCaps } from 'appium-uiautomator2-driver/build/lib/types'; import dotenv from 'dotenv'; import { isString } from 'lodash'; @@ -16,20 +15,21 @@ export const androidAppActivity = 'network.loki.messenger.RoutingActivity'; console.log(`Android app full path: ${androidAppFullPath}`); -const sharedCapabilities: AppiumAndroidCapabilities & AppiumCapabilities = { +const sharedCapabilities: W3CUiautomator2DriverCaps['alwaysMatch'] = { 'appium:app': androidAppFullPath, - 'appium:platformName': 'Android', + platformName: 'Android', 'appium:platformVersion': '14', 'appium:appPackage': androidAppPackage, 'appium:appActivity': androidAppActivity, 'appium:automationName': 'UiAutomator2', 'appium:newCommandTimeout': 300000, 'appium:eventTimings': false, + 'appium:injectedImageProperties': {}, }; const udids = ['emulator-5554', 'emulator-5556', 'emulator-5558', 'emulator-5560']; -const emulatorCapabilities: AppiumCapabilities[] = udids.map(udid => ({ +const emulatorCapabilities: W3CUiautomator2DriverCaps['alwaysMatch'][] = udids.map(udid => ({ ...sharedCapabilities, 'appium:udid': udid, })); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index c269d9cf4..6de997f19 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -324,6 +324,17 @@ export class DeviceWrapper { return this.toShared().getPageSource(); } + public async injectQRCodeImage(imagePath: string): Promise { + if (this.isAndroid()) { + const base64Image = (await fs.readFile(imagePath)).toString('base64'); + await this.toShared().execute('mobile: injectEmulatorCameraImage', { + payload: base64Image, + }); + this.log(`Added ${imagePath}`); + } + // iOS: no-op — camera pipeline unavailable on simulator + } + /* === all the device-specific function === */ // ELEMENT INTERACTION diff --git a/scripts/resources/Toren1BD.posters b/scripts/resources/Toren1BD.posters new file mode 100644 index 000000000..fc87492bb --- /dev/null +++ b/scripts/resources/Toren1BD.posters @@ -0,0 +1,11 @@ +poster wall +size 2 2 +position -0.807 0.320 5.316 +rotation 0 -150 0 +default poster.png + +poster table +size 1 1 +position 0 0 -1.5 +rotation 0 0 0 +default placeholder.png \ No newline at end of file diff --git a/scripts/resources/placeholder.png b/scripts/resources/placeholder.png new file mode 100644 index 000000000..03c66ba47 --- /dev/null +++ b/scripts/resources/placeholder.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:885ca9b7463b30b0b13d5bf0f874084282d674ca5e07d3e0a3f897f8c509b847 +size 53060 diff --git a/scripts/setup_virtual_scene.ts b/scripts/setup_virtual_scene.ts new file mode 100644 index 000000000..121fb4e86 --- /dev/null +++ b/scripts/setup_virtual_scene.ts @@ -0,0 +1,48 @@ +/** + * Copies virtual scene config files from the repo to local Android SDK folder if necessary. + * + * The Toren1BD.posters file keeps track of where to show posters (images) in the virtual camera scene. + * It has been modified so that the `table` poster shows right in front of where the camera opens (x: 0, y: 0, z: -1.5). + * This is necessary because appium's injection methods manipulate this specific poster's image content. + * This has been the only reliable way to get this working. + * + * The file is global for all emulators on the host machine but each appium session can temporarily modify the image. + * + * CI: This script runs before emulator boot. + * Local dev: Run `pnpm setup-virtual-scene` once and reboot emulators for the changes to take effect. + */ + +import { copyFileSync, readFileSync } from 'fs'; +import path from 'path'; + +const sdkRoot = process.env.ANDROID_SDK_ROOT; +if (!sdkRoot) { + throw new Error('ANDROID_SDK_ROOT is not set'); +} + +const resourcesDir = path.join(sdkRoot, 'emulator', 'resources'); + +const files = ['placeholder.png', 'Toren1BD.posters']; + +function syncFile(filename: string) { + const repoFile = path.join(__dirname, 'resources', filename); + const sdkFile = path.join(resourcesDir, filename); + + const repoContent = readFileSync(repoFile); + + let needsCopy = true; + try { + needsCopy = !repoContent.equals(readFileSync(sdkFile)); + } catch { + // File doesn't exist in SDK yet + } + + if (!needsCopy) { + console.log(`${filename} already up to date`); + } else { + copyFileSync(repoFile, sdkFile); + console.log(`${filename} updated at ${sdkFile}`); + } +} + +files.forEach(syncFile); From d5baaec98cca1c042bf7c11ef132e1db2dbd3c06 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Mar 2026 16:40:31 +1100 Subject: [PATCH 146/184] feat: add qr code tests --- run/test/locators/index.ts | 29 ++++++ run/test/locators/settings.ts | 14 +++ run/test/specs/qr_codes.spec.ts | 158 +++++++++++++++++++++++++++++ run/types/DeviceWrapper.ts | 10 +- run/types/testing.ts | 1 + scripts/resources/Toren1BD.posters | 2 +- scripts/setup_virtual_scene.ts | 7 +- 7 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 run/test/specs/qr_codes.spec.ts diff --git a/run/test/locators/index.ts b/run/test/locators/index.ts index 267b1d1ca..f61272126 100644 --- a/run/test/locators/index.ts +++ b/run/test/locators/index.ts @@ -1,4 +1,5 @@ import { ANDROID_XPATHS, IOS_XPATHS } from '../../constants'; +import { tStripped } from '../../localizer/lib'; import { DeviceWrapper } from '../../types/DeviceWrapper'; import { StrategyExtractionObj } from '../../types/testing'; import { getAppDisplayName } from '../utils/devnet'; @@ -326,6 +327,20 @@ export class GIFName extends LocatorsInterface { } } +export class GrantCameraAccessButton extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: `new UiSelector().text("${tStripped('cameraGrantAccess')}")`, + } as const; + case 'ios': + throw new Error('Not implemented on iOS'); + } + } +} + export class ImageName extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { @@ -482,6 +497,20 @@ export class ReadReceiptsButton extends LocatorsInterface { } } +export class ScanQRTab extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: `new UiSelector().text("${tStripped('qrScan')}")`, + } as const; + case 'ios': + throw new Error('Not implemented on iOS'); + } + } +} + export class ShareExtensionIcon extends LocatorsInterface { public build(): StrategyExtractionObj { switch (this.platform) { diff --git a/run/test/locators/settings.ts b/run/test/locators/settings.ts index 8e6fdd56a..6db4471ce 100644 --- a/run/test/locators/settings.ts +++ b/run/test/locators/settings.ts @@ -385,6 +385,20 @@ export class VersionNumber extends LocatorsInterface { } } +export class ViewQR extends LocatorsInterface { + public build() { + switch (this.platform) { + case 'android': + return { + strategy: '-android uiautomator', + selector: `new UiSelector().text("${tStripped('qrView')}")`, + } as const; + case 'ios': + throw new Error('Not implemented on iOS'); + } + } +} + export class YesButton extends LocatorsInterface { public build() { switch (this.platform) { diff --git a/run/test/specs/qr_codes.spec.ts b/run/test/specs/qr_codes.spec.ts new file mode 100644 index 000000000..81d1474df --- /dev/null +++ b/run/test/specs/qr_codes.spec.ts @@ -0,0 +1,158 @@ +import { test, type TestInfo } from '@playwright/test'; + +import { communities } from '../../constants/community'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { InteractionPoints, USERNAME } from '../../types/testing'; +import { GrantCameraAccessButton, ImagePermissionsModalAllow, ScanQRTab } from '../locators'; +import { ConversationHeaderName, ConversationSettings } from '../locators/conversation'; +import { AccountIDDisplay, ContinueButton } from '../locators/global'; +import { PlusButton } from '../locators/home'; +import { AccountRestoreButton, FastModeRadio } from '../locators/onboarding'; +import { RecoveryPasswordMenuItem, UserSettings, ViewQR } from '../locators/settings'; +import { JoinCommunityOption, NewMessageOption } from '../locators/start_conversation'; +import { open_Alice1_bob1_notfriends } from '../state_builder'; +import { assert, clickOnCoordinates, sleepFor } from '../utils'; +import { joinCommunity } from '../utils/community'; +import { newUser } from '../utils/create_account'; +import { truncatePubkey } from '../utils/get_account_id'; +import { closeApp, openAppTwoDevices, SupportedPlatformsType } from '../utils/open_app'; +import { handleNotificationPermissions } from '../utils/permissions'; + +androidIt({ + title: 'Restore account from QR code', + risk: 'high', + testCb: qrCodeSeedPhrase, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'Onboarding', + suite: 'Restore account', + }, + allureDescription: 'Verifies that an account can be restored on a second device by scanning a recovery phrase QR code', +}); + +androidIt({ + title: 'New Conversation from QR code', + risk: 'high', + testCb: qrCodeAccountID, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'New Conversation', + suite: 'New Message', + }, + allureDescription: `Verifies that a new conversation can be started by scanning another user's Account ID QR code`, +}); + +androidIt({ + title: 'Join Community from QR code', + risk: 'medium', + testCb: qrCodeCommunity, + countOfDevicesNeeded: 2, + allureSuites: { + parent: 'New Conversation', + suite: 'Join Community', + }, + allureDescription: 'Verifies that a community can be joined by scanning a community QR code', +}); + +async function qrCodeSeedPhrase(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device1, device2 } = await openAppTwoDevices(platform, testInfo); + const firstAccountID = await test.step(TestSteps.SETUP.NEW_USER, async () => { + await newUser(device1, USERNAME.ALICE, { saveUserData: false }); + await device1.clickOnElementAll(new UserSettings(device1)); + const firstAccountIDElement = await device1.waitForTextElementToBePresent( + new AccountIDDisplay(device1) + ); + return device1.getTextFromElement(firstAccountIDElement); + }); + const base64 = await test.step(TestSteps.OPEN.GENERIC('Recovery Password QR code'), async () => { + await device1.clickOnElementAll(new RecoveryPasswordMenuItem(device1)); + await device1.clickOnElementAll(new ViewQR(device1)); + await sleepFor(500); + return device1.getScreenshot(); + }); + await test.step(TestSteps.SETUP.RESTORE_ACCOUNT(USERNAME.ALICE), async () => { + await device2.injectImageToScene(base64); + await device2.clickOnElementAll(new AccountRestoreButton(device2)); + await device2.clickOnElementAll(new ScanQRTab(device2)); + await device2.clickOnElementAll(new GrantCameraAccessButton(device2)); + await device2.clickOnElementAll(new ImagePermissionsModalAllow(device2)); + await device2.clickOnElementAll(new FastModeRadio(device2)); + await device2.clickOnElementAll(new ContinueButton(device2)); + await handleNotificationPermissions(device2, true); + }); + await test.step('Verify the correct account has been restored', async () => { + await device2.clickOnElementAll(new UserSettings(device2)); + const secondAccountIDElement = await device2.waitForTextElementToBePresent( + new AccountIDDisplay(device2) + ); + const secondAccountID = await device2.getTextFromElement(secondAccountIDElement); + assert(firstAccountID, 'The account recovered from QR code is not the right one').toBe( + secondAccountID + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device1, device2); + }); +} + +async function qrCodeAccountID(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { + devices: { alice1, bob1 }, + prebuilt: { alice }, + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_bob1_notfriends({ platform, testInfo }); + }); + const base64 = await test.step(TestSteps.OPEN.GENERIC('Account ID QR code'), async () => { + await alice1.clickOnElementAll(new PlusButton(alice1)); + await sleepFor(500); + return alice1.getScreenshot(); + }); + await test.step(TestSteps.NEW_CONVERSATION.NEW_MESSAGE, async () => { + await bob1.injectImageToScene(base64); + await bob1.clickOnElementAll(new PlusButton(bob1)); + await bob1.clickOnElementAll(new NewMessageOption(bob1)); + await bob1.clickOnElementAll(new ScanQRTab(bob1)); + await bob1.clickOnElementAll(new GrantCameraAccessButton(bob1)); + await bob1.clickOnElementAll(new ImagePermissionsModalAllow(bob1)); + }); + await test.step(`Verify conversation with ${alice.userName} opened`, async () => { + const truncatedPubkey = truncatePubkey(alice.accountID, platform); + await bob1.waitForTextElementToBePresent(new ConversationHeaderName(bob1, truncatedPubkey)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} + +async function qrCodeCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { + devices: { alice1, bob1 }, + prebuilt: { bob } + } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { + return open_Alice1_bob1_notfriends({ platform, testInfo }); + }); + const base64 = await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + await joinCommunity(alice1, communities.testCommunity.link, communities.testCommunity.name); + await alice1.clickOnElementAll(new ConversationSettings(alice1)); + await clickOnCoordinates(alice1, InteractionPoints.AndroidConvoSettingsQRCode); + await sleepFor(500); + return alice1.getScreenshot(); + }); + await test.step(`${bob.userName} joins community via QR scan`, async () => { + await bob1.clickOnElementAll(new PlusButton(bob1)); + await bob1.injectImageToScene(base64); + await bob1.clickOnElementAll(new JoinCommunityOption(bob1)); + await bob1.clickOnElementAll(new ScanQRTab(bob1)); + await bob1.clickOnElementAll(new GrantCameraAccessButton(bob1)); + await bob1.clickOnElementAll(new ImagePermissionsModalAllow(bob1)); + }); + await test.step(`Verify ${bob.userName} joined the community`, async () => { + await bob1.waitForTextElementToBePresent( + new ConversationHeaderName(bob1, communities.testCommunity.name) + ); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(alice1, bob1); + }); +} diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 6de997f19..b2d839158 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -324,15 +324,17 @@ export class DeviceWrapper { return this.toShared().getPageSource(); } - public async injectQRCodeImage(imagePath: string): Promise { + /** + * Injects a base64-encoded image into the Android emulator's virtual camera scene. + */ + public async injectImageToScene(base64Image: string): Promise { if (this.isAndroid()) { - const base64Image = (await fs.readFile(imagePath)).toString('base64'); await this.toShared().execute('mobile: injectEmulatorCameraImage', { payload: base64Image, }); - this.log(`Added ${imagePath}`); + this.log(`Injected image to scene`); } - // iOS: no-op — camera pipeline unavailable on simulator + // iOS: no-op } /* === all the device-specific function === */ diff --git a/run/types/testing.ts b/run/types/testing.ts index f6f6d6fd6..4059c79cc 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -46,6 +46,7 @@ export type Coordinates = { export const InteractionPoints: Record = { BackToSession: { x: 42, y: 42 }, + AndroidConvoSettingsQRCode: { x: 627, y: 329} }; export type Strategy = '-android uiautomator' | 'accessibility id' | 'class name' | 'id' | 'xpath'; diff --git a/scripts/resources/Toren1BD.posters b/scripts/resources/Toren1BD.posters index fc87492bb..ff975f61d 100644 --- a/scripts/resources/Toren1BD.posters +++ b/scripts/resources/Toren1BD.posters @@ -5,7 +5,7 @@ rotation 0 -150 0 default poster.png poster table -size 1 1 +size 2 2 position 0 0 -1.5 rotation 0 0 0 default placeholder.png \ No newline at end of file diff --git a/scripts/setup_virtual_scene.ts b/scripts/setup_virtual_scene.ts index 121fb4e86..059e7eb34 100644 --- a/scripts/setup_virtual_scene.ts +++ b/scripts/setup_virtual_scene.ts @@ -2,9 +2,10 @@ * Copies virtual scene config files from the repo to local Android SDK folder if necessary. * * The Toren1BD.posters file keeps track of where to show posters (images) in the virtual camera scene. - * It has been modified so that the `table` poster shows right in front of where the camera opens (x: 0, y: 0, z: -1.5). - * This is necessary because appium's injection methods manipulate this specific poster's image content. - * This has been the only reliable way to get this working. + * It has been modified so that the `table` poster shows right in front of where the camera opens, + * scaled up 2x, positioned at x: 0, y: 0, z: -1.5. + * This is necessary because appium's injection method manipulates this specific poster's image content. + * This has been the only reliable way to get this working other than patching appium and the android driver. * * The file is global for all emulators on the host machine but each appium session can temporarily modify the image. * From 0eab7cdbb2f98d5b4859cd4a15e424c640cf52ff Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 11:27:33 +1100 Subject: [PATCH 147/184] fix: emulators must have virtual scene configured --- scripts/ci.sh | 18 +++++++++++++----- scripts/setup_virtual_scene.ts | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 647c403e1..369231110 100644 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -57,18 +57,26 @@ function create_emulators() { # Set the RAM size to 4GB (4192MB) sed -i 's/^hw\.ramSize=.*/hw.ramSize=4192/' "$CONFIG_FILE" + # Use virtualscene camera so injectEmulatorCameraImage works + sed -i 's/^hw\.camera\.back=.*/hw.camera.back=virtualscene/' "$CONFIG_FILE" done cd } +# Start a single emulator to snapshot or start all 4 at once function start_for_snapshots() { - for i in {1..4} - do - DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-load & - sleep 20 - done + local i=${1:-} + if [[ -n "$i" ]]; then + DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-load & + else + for i in {1..4} + do + DISPLAY=:0 emulator @emulator$i -gpu host -accel on -no-snapshot-load & + sleep 20 + done + fi } # let the emulators start and be ready (check cpu usage) before calling this. diff --git a/scripts/setup_virtual_scene.ts b/scripts/setup_virtual_scene.ts index 059e7eb34..67c8901f2 100644 --- a/scripts/setup_virtual_scene.ts +++ b/scripts/setup_virtual_scene.ts @@ -1,6 +1,9 @@ /** * Copies virtual scene config files from the repo to local Android SDK folder if necessary. * + * NOTE: This only works if the emulators' back camera is set to `virtualscene`: + * The config.ini must have a `hw.camera.back=virtualscene` entry. + * * The Toren1BD.posters file keeps track of where to show posters (images) in the virtual camera scene. * It has been modified so that the `table` poster shows right in front of where the camera opens, * scaled up 2x, positioned at x: 0, y: 0, z: -1.5. From ab7dc40374e2a00c279d8b94a9c7ee8df401350c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 11:33:33 +1100 Subject: [PATCH 148/184] chore: linting --- run/test/specs/qr_codes.spec.ts | 5 +++-- run/types/testing.ts | 2 +- scripts/setup_virtual_scene.ts | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/run/test/specs/qr_codes.spec.ts b/run/test/specs/qr_codes.spec.ts index 81d1474df..bc9a9f703 100644 --- a/run/test/specs/qr_codes.spec.ts +++ b/run/test/specs/qr_codes.spec.ts @@ -28,7 +28,8 @@ androidIt({ parent: 'Onboarding', suite: 'Restore account', }, - allureDescription: 'Verifies that an account can be restored on a second device by scanning a recovery phrase QR code', + allureDescription: + 'Verifies that an account can be restored on a second device by scanning a recovery phrase QR code', }); androidIt({ @@ -128,7 +129,7 @@ async function qrCodeAccountID(platform: SupportedPlatformsType, testInfo: TestI async function qrCodeCommunity(platform: SupportedPlatformsType, testInfo: TestInfo) { const { devices: { alice1, bob1 }, - prebuilt: { bob } + prebuilt: { bob }, } = await test.step(TestSteps.SETUP.QA_SEEDER, async () => { return open_Alice1_bob1_notfriends({ platform, testInfo }); }); diff --git a/run/types/testing.ts b/run/types/testing.ts index 4059c79cc..72ad7c5bb 100644 --- a/run/types/testing.ts +++ b/run/types/testing.ts @@ -46,7 +46,7 @@ export type Coordinates = { export const InteractionPoints: Record = { BackToSession: { x: 42, y: 42 }, - AndroidConvoSettingsQRCode: { x: 627, y: 329} + AndroidConvoSettingsQRCode: { x: 627, y: 329 }, }; export type Strategy = '-android uiautomator' | 'accessibility id' | 'class name' | 'id' | 'xpath'; diff --git a/scripts/setup_virtual_scene.ts b/scripts/setup_virtual_scene.ts index 67c8901f2..8b6eb9042 100644 --- a/scripts/setup_virtual_scene.ts +++ b/scripts/setup_virtual_scene.ts @@ -2,11 +2,11 @@ * Copies virtual scene config files from the repo to local Android SDK folder if necessary. * * NOTE: This only works if the emulators' back camera is set to `virtualscene`: - * The config.ini must have a `hw.camera.back=virtualscene` entry. - * + * The config.ini must have a `hw.camera.back=virtualscene` entry. + * * The Toren1BD.posters file keeps track of where to show posters (images) in the virtual camera scene. - * It has been modified so that the `table` poster shows right in front of where the camera opens, - * scaled up 2x, positioned at x: 0, y: 0, z: -1.5. + * It has been modified so that the `table` poster shows right in front of where the camera opens, + * scaled up 2x, positioned at x: 0, y: 0, z: -1.5. * This is necessary because appium's injection method manipulates this specific poster's image content. * This has been the only reliable way to get this working other than patching appium and the android driver. * From 3ec4f5dcdda5e7f9bdb10ef300cae85be2635695 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 14:00:29 +1100 Subject: [PATCH 149/184] chore: add shorthand for mock pro --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 055fac0d3..512dcd945 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "test-high-risk-ios": "_TESTING=1 npx playwright test --grep '@ios @high-risk'", "start-server": "./node_modules/.bin/appium server --use-drivers=uiautomator2,xcuitest --port 8110 --allow-cors", "allure-generate": "allure generate allure/allure-results --clean", - "allure-open": "allure open" + "allure-open": "allure open", + "mock-pro": "npx ts-node run/test/utils/mock_pro.ts" }, "devDependencies": { "@appium/execute-driver-plugin": "^5.1.0", From a11e57c84c23b112fd1074c39e4acd6933bce95e Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 14:00:56 +1100 Subject: [PATCH 150/184] fix: add disappearing timer buffer --- run/test/specs/disappearing_messages_follow_settings.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run/test/specs/disappearing_messages_follow_settings.spec.ts b/run/test/specs/disappearing_messages_follow_settings.spec.ts index 4b8a68b87..b668ad15a 100644 --- a/run/test/specs/disappearing_messages_follow_settings.spec.ts +++ b/run/test/specs/disappearing_messages_follow_settings.spec.ts @@ -28,6 +28,7 @@ bothPlatformsIt({ const time = DISAPPEARING_TIMES.THIRTY_SECONDS; const timerType = 'Disappear after send option'; +const disappearMaxWait = 35_000; // 30s plus buffer async function disappearingMessagesFollowSetting1o1( platform: SupportedPlatformsType, @@ -64,10 +65,12 @@ async function disappearingMessagesFollowSetting1o1( device.hasElementDisappeared({ ...new MessageBody(device, aliceMsg).build(), actualStartTime: aliceTimestamp, + maxWait: disappearMaxWait, }), device.hasElementDisappeared({ ...new MessageBody(device, bobMsg).build(), actualStartTime: bobTimestamp, + maxWait: disappearMaxWait, }), ]) ); From bf36482981f11a79c027df43cc88ecc8217fd030 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 14:41:44 +1100 Subject: [PATCH 151/184] chore: tidy up allure utils --- run/test/utils/allure/allureHelpers.ts | 4 ++++ run/test/utils/allure/publishReport.ts | 14 +++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/run/test/utils/allure/allureHelpers.ts b/run/test/utils/allure/allureHelpers.ts index c3b2971f1..6993aaeb2 100644 --- a/run/test/utils/allure/allureHelpers.ts +++ b/run/test/utils/allure/allureHelpers.ts @@ -75,6 +75,7 @@ export function getReportContextFromEnv(): ReportContext { githubRunUrl, }; } + // The Environment block shows up in the report dashboard export async function writeEnvironmentProperties(ctx: ReportContext) { await fs.ensureDir(allureResultsDir); @@ -90,6 +91,7 @@ export async function writeEnvironmentProperties(ctx: ReportContext) { 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) { @@ -110,6 +112,7 @@ export async function writeExecutorJson(ctx: ReportContext) { ); 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 = { @@ -183,6 +186,7 @@ function getGitCommitSha(): string { function getGitBranch(): string { return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); } + // Handle test-level metadata such as suites, test description or linked issues export async function setupAllureTestInfo({ suites, diff --git a/run/test/utils/allure/publishReport.ts b/run/test/utils/allure/publishReport.ts index db6e94182..8a795eebc 100644 --- a/run/test/utils/allure/publishReport.ts +++ b/run/test/utils/allure/publishReport.ts @@ -12,19 +12,12 @@ import { // Bail out early if not on CI if (process.env.CI !== '1' || process.env.ALLURE_ENABLED === 'false') { - console.log('Skipping closeRun (CI != 1 or ALLURE_ENABLED is false)'); + console.log('Skipping publishReport (CI != 1 or ALLURE_ENABLED is false)'); process.exit(0); } // Publishes the report directory to the gh-pages branch of the repo function publishToGhPages(dir: string, dest: string, repo: string, message: string): Promise { - // Ensure .nojekyll file exists to skip Jekyll processing - const nojekyllPath = path.join(dir, '.nojekyll'); - if (!fs.existsSync(nojekyllPath)) { - fs.writeFileSync(nojekyllPath, ''); - console.log('Created .nojekyll file'); - } - return new Promise((resolve, reject) => { void ghpages.publish( dir, @@ -61,7 +54,7 @@ async function publishReport() { const publishedReportName = ctx.reportFolder; const newReportDir = path.join(ctx.platform, publishedReportName); - // Allue manipulation + // Allure manipulation await patchStylesCss(); await patchFilesForLFSCDN(ctx); @@ -86,6 +79,9 @@ async function publishReport() { console.log(`Deploying report to GitHub Pages as: ${publishedReportName}`); + // Clear stale gh-pages cache left by interrupted runs + ghpages.clean(); + // Publish the report to GitHub Pages try { await publishToGhPages( From 30f61d579ad4e184bdba535ca3fe7cd5eba634f6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 15:07:10 +1100 Subject: [PATCH 152/184] fix: always dismiss cta after making account pro --- run/test/specs/user_actions_animated_profile_picture.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run/test/specs/user_actions_animated_profile_picture.spec.ts b/run/test/specs/user_actions_animated_profile_picture.spec.ts index 695eeb04e..c96e5a7b2 100644 --- a/run/test/specs/user_actions_animated_profile_picture.spec.ts +++ b/run/test/specs/user_actions_animated_profile_picture.spec.ts @@ -89,6 +89,7 @@ async function proActivatedCTA(platform: SupportedPlatformsType, testInfo: TestI }); await makeAccountPro({ user: alice, platform }); await forceStopAndRestart(device); + await device.dismissCTA(); await test.step('Verify Pro Activated CTA', async () => { await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new UserAvatar(device)); @@ -110,6 +111,7 @@ async function proAnimatedDP(platform: SupportedPlatformsType, testInfo: TestInf }); await makeAccountPro({ user: alice, platform }); await forceStopAndRestart(device); + await device.dismissCTA(); await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { await device.uploadProfilePicture(true); }); @@ -134,6 +136,7 @@ async function proAnimatedDPShows(platform: SupportedPlatformsType, testInfo: Te const { alice, bob } = prebuilt; await makeAccountPro({ user: alice, platform }); await forceStopAndRestart(alice1); + await alice1.dismissCTA(); await test.step(TestSteps.USER_ACTIONS.CHANGE_PROFILE_PICTURE, async () => { await alice1.uploadProfilePicture(true); }); From 3752ba0bde135988d4f7f1ae94dd3a174c3473c1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 6 Mar 2026 17:41:49 +1100 Subject: [PATCH 153/184] fix: scroll down where needed on iOS --- run/test/specs/visual_settings.spec.ts | 1 + run/types/DeviceWrapper.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/run/test/specs/visual_settings.spec.ts b/run/test/specs/visual_settings.spec.ts index 9050a88f9..16d4b021f 100644 --- a/run/test/specs/visual_settings.spec.ts +++ b/run/test/specs/visual_settings.spec.ts @@ -45,6 +45,7 @@ const testCases = [ screenshotFile: 'settings_notifications', navigation: async (device: DeviceWrapper) => { await device.clickOnElementAll(new UserSettings(device)); + await device.onIOS().scrollDown(); await device.clickOnElementAll(new NotificationsMenuItem(device)); await sleepFor(1_000); // This one otherwise captures a black screen }, diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index c269d9cf4..7a4b0846f 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2702,6 +2702,7 @@ export class DeviceWrapper { public async getVersionNumber() { // NOTE if this becomes necessary for more tests, consider adding a property/caching to the DeviceWrapper await this.clickOnElementAll(new UserSettings(this)); + await this.onIOS().scrollDown(); const versionElement = await this.waitForTextElementToBePresent(new VersionNumber(this)); // Get the full text from the element const versionText = await this.getTextFromElement(versionElement); From 4bf02199532ab0e5bef68bebcf7d89643d73abb2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 10 Mar 2026 10:29:28 +1100 Subject: [PATCH 154/184] fix: platform inconsistencies both platforms accept message request with button both platforms scroll up after hiding recovery password --- run/test/specs/user_actions_create_contact.spec.ts | 2 -- run/test/specs/user_actions_hide_recovery_password.spec.ts | 2 +- run/types/DeviceWrapper.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/run/test/specs/user_actions_create_contact.spec.ts b/run/test/specs/user_actions_create_contact.spec.ts index 368e1bb2c..782223b5b 100644 --- a/run/test/specs/user_actions_create_contact.spec.ts +++ b/run/test/specs/user_actions_create_contact.spec.ts @@ -30,8 +30,6 @@ async function createContact(platform: SupportedPlatformsType, testInfo: TestInf await device2.acceptMessageRequestWithButton(); - // Type into message input box - await device2.sendMessage(`Reply-message-${Bob.userName}-to-${Alice.userName}`); // NOTE: This appears to be broken on both platforms: // Verify config message states message request was accepted // "messageRequestsAccepted": "Your message request has been accepted.", 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 5e04c5a89..6a564b7c3 100644 --- a/run/test/specs/user_actions_hide_recovery_password.spec.ts +++ b/run/test/specs/user_actions_hide_recovery_password.spec.ts @@ -48,7 +48,7 @@ async function hideRecoveryPassword(platform: SupportedPlatformsType, testInfo: maxWait: 1000, }); // Should be taken back to Settings page after hiding recovery password - await device1.onAndroid().scrollUp(); + await device1.scrollUp(); await device1.waitForTextElementToBePresent(new AccountIDDisplay(device1)); // Check that linked device still has Recovery Password await device2.clickOnElementAll(new UserSettings(device2)); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 7a4b0846f..473dacf1b 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -1817,7 +1817,7 @@ export class DeviceWrapper { public async acceptMessageRequestWithButton() { await this.clickOnElementAll(new MessageRequestsBanner(this)); await this.clickOnElementAll(new MessageRequestItem(this)); - await this.onAndroid().clickOnElementAll(new AcceptMessageRequestButton(this)); + await this.clickOnElementAll(new AcceptMessageRequestButton(this)); } // TODO instead of blind sleeping, check presence of reply preview From 3efbbeb1b1f5b7a47cbdb6ffc0891837ef88a5cf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Mar 2026 10:25:12 +1100 Subject: [PATCH 155/184] fix: lfs keychain issues on macos runner and prune report directories - disable lfs lock verification on publish step (macOS keychain error -25308) - disable credential helper on fetch-allure-history clone (same root cause) - extend prune workflow to also delete report dirs older than 60 days - refactor prune step to use shared bash functions (is_older_than, apply_deletions) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/prune-attachments.yml | 92 +++++++++++-------- .../actions/fetch-allure-history/action.yml | 2 + .../generate-publish-test-report/action.yml | 1 + 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/.github/workflows/prune-attachments.yml b/.github/workflows/prune-attachments.yml index 1fedfe1e7..c39efbf65 100644 --- a/.github/workflows/prune-attachments.yml +++ b/.github/workflows/prune-attachments.yml @@ -1,14 +1,19 @@ -name: Prune Old Allure Attachments +name: Prune Old Allure Reports on: schedule: - cron: '0 16 * * 0' # Weekly, 16:00 UTC Sunday workflow_dispatch: inputs: - retention-days: + attachment-retention-days: description: 'Number of days to retain attachments' required: false default: '14' type: string + report-retention-days: + description: 'Number of days to retain full report directories' + required: false + default: '60' + type: string dry-run: description: 'If true, only list files that would be deleted (no deletion or commit)' required: false @@ -28,50 +33,65 @@ jobs: env: GIT_LFS_SKIP_SMUDGE: 1 # Don't actually download LFS content - - name: Set retention days - id: retention - run: | - DAYS="${{ github.event.inputs.retention-days || '14' }}" - [[ "$DAYS" =~ ^[1-9][0-9]*$ ]] || { echo "Error: retention-days must be a positive integer"; exit 1; } - echo "days=$DAYS" >> $GITHUB_OUTPUT - - - name: Prune Allure attachments + - name: Prune old attachments and report directories env: DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }} - RETENTION_DAYS: ${{ steps.retention.outputs.days }} + ATTACHMENT_DAYS: ${{ github.event.inputs.attachment-retention-days || '14' }} + REPORT_DAYS: ${{ github.event.inputs.report-retention-days || '60' }} run: | - CUTOFF_DATE=$(date -d "$RETENTION_DAYS days ago" +%s) + [[ "$ATTACHMENT_DAYS" =~ ^[1-9][0-9]*$ ]] || { echo "Error: retention-days must be a positive integer"; exit 1; } + [[ "$REPORT_DAYS" =~ ^[1-9][0-9]*$ ]] || { echo "Error: report-retention-days must be a positive integer"; exit 1; } + PREVIEW_LIMIT=20 - DELETE_LIST="files_to_delete.txt" - echo "Retention: $RETENTION_DAYS days | Dry run: $DRY_RUN" >> $GITHUB_STEP_SUMMARY + # Returns 0 (true) if the given path's last commit is older than the given cutoff epoch + is_older_than() { + local path="$1" cutoff="$2" + local commit_date + commit_date=$(git log -1 --format="%ct" -- "$path" 2>/dev/null || echo "0") + [ "$commit_date" -ne "0" ] && [ "$commit_date" -lt "$cutoff" ] + } + + # Previews or deletes items listed in a file, then summarises + apply_deletions() { + local list="$1" label="$2" + local count + count=$(wc -l < "$list" 2>/dev/null || echo 0) + echo "Found $count old $label" >> $GITHUB_STEP_SUMMARY + if [ "$count" -gt 0 ]; then + if [ "$DRY_RUN" == "true" ]; then + echo "$label to delete:" >> $GITHUB_STEP_SUMMARY + head -$PREVIEW_LIMIT "$list" >> $GITHUB_STEP_SUMMARY + [ "$count" -gt $PREVIEW_LIMIT ] && echo "...(+$((count-PREVIEW_LIMIT)) more)" >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r item; do rm -rf "$item"; done < "$list" + echo "Deleted $count $label" >> $GITHUB_STEP_SUMMARY + fi + fi + rm -f "$list" + } - # Find old attachment files - > "$DELETE_LIST" + echo "Attachment retention: $ATTACHMENT_DAYS days | Report retention: $REPORT_DAYS days | Dry run: $DRY_RUN" >> $GITHUB_STEP_SUMMARY + + # Prune attachment files older than ATTACHMENT_DAYS + ATTACHMENT_CUTOFF=$(date -d "$ATTACHMENT_DAYS days ago" +%s) + ATTACHMENT_LIST="attachments_to_delete.txt" + > "$ATTACHMENT_LIST" for pattern in '*/data/attachments/*.png' '*/data/attachments/*.txt' '*/data/attachments/*.imagediff'; do while IFS= read -r -d '' file; do - COMMIT_DATE=$(git log -1 --format="%ct" -- "$file" 2>/dev/null || echo "0") - [ "$COMMIT_DATE" -lt "$CUTOFF_DATE" ] && [ "$COMMIT_DATE" -ne "0" ] && echo "$file" >> "$DELETE_LIST" + is_older_than "$file" "$ATTACHMENT_CUTOFF" && echo "$file" >> "$ATTACHMENT_LIST" done < <(git ls-files -z "$pattern") done + apply_deletions "$ATTACHMENT_LIST" "attachment files" - COUNT=$(wc -l < "$DELETE_LIST" 2>/dev/null || echo 0) - echo "Found $COUNT old files" >> $GITHUB_STEP_SUMMARY - - if [ "$COUNT" -gt 0 ]; then - if [ "$DRY_RUN" == "true" ]; then - echo "Files to delete:" >> $GITHUB_STEP_SUMMARY - if [ -s "$DELETE_LIST" ]; then - head -$PREVIEW_LIMIT "$DELETE_LIST" >> $GITHUB_STEP_SUMMARY - [ "$COUNT" -gt $PREVIEW_LIMIT ] && echo "...(+$((COUNT-PREVIEW_LIMIT)) more)" >> $GITHUB_STEP_SUMMARY - fi - else - xargs rm -f < "$DELETE_LIST" - echo "Deleted $COUNT files" >> $GITHUB_STEP_SUMMARY - fi - fi - - rm -f "$DELETE_LIST" + # Prune report directories older than REPORT_DAYS + REPORT_CUTOFF=$(date -d "$REPORT_DAYS days ago" +%s) + REPORT_LIST="reports_to_delete.txt" + > "$REPORT_LIST" + for dir in android/run-* ios/run-*; do + [ -d "$dir" ] && is_older_than "$dir" "$REPORT_CUTOFF" && echo "$dir" >> "$REPORT_LIST" + done + apply_deletions "$REPORT_LIST" "report directories" - name: Commit changes if: github.event.inputs.dry-run != 'true' @@ -81,7 +101,7 @@ jobs: if [ -n "$(git status --porcelain)" ]; then git add -A - git commit -m "ci: Prune attachments older than ${{ steps.retention.outputs.days }} days" + git commit -m "ci: prune old attachments and report directories" git push fi diff --git a/github/actions/fetch-allure-history/action.yml b/github/actions/fetch-allure-history/action.yml index f0558369f..ddd246c31 100644 --- a/github/actions/fetch-allure-history/action.yml +++ b/github/actions/fetch-allure-history/action.yml @@ -18,6 +18,8 @@ runs: echo "Clearing temp clone" rm -rf "$CLONE_DIR" + git config --global credential.helper "" + echo "Cloning gh-pages branch" GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 --branch gh-pages "$GH_REPO" "$CLONE_DIR" diff --git a/github/actions/generate-publish-test-report/action.yml b/github/actions/generate-publish-test-report/action.yml index fccc2bf99..86b41fc68 100644 --- a/github/actions/generate-publish-test-report/action.yml +++ b/github/actions/generate-publish-test-report/action.yml @@ -40,6 +40,7 @@ runs: id: publish shell: bash run: | + git config --global lfs.locksverify false npx ts-node run/test/utils/allure/publishReport.ts env: PLATFORM: ${{ inputs.PLATFORM }} From 7a1a620a1fce0a0380d59e8210bb4b29ca8bc877 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Mar 2026 10:31:51 +1100 Subject: [PATCH 156/184] chore: add file count to prune summary --- .github/workflows/prune-attachments.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prune-attachments.yml b/.github/workflows/prune-attachments.yml index c39efbf65..b48e9ac62 100644 --- a/.github/workflows/prune-attachments.yml +++ b/.github/workflows/prune-attachments.yml @@ -91,6 +91,8 @@ jobs: for dir in android/run-* ios/run-*; do [ -d "$dir" ] && is_older_than "$dir" "$REPORT_CUTOFF" && echo "$dir" >> "$REPORT_LIST" done + REPORT_FILE_COUNT=$(while IFS= read -r dir; do find "$dir" -type f; done < "$REPORT_LIST" | wc -l) + echo "($REPORT_FILE_COUNT files total)" >> $GITHUB_STEP_SUMMARY apply_deletions "$REPORT_LIST" "report directories" - name: Commit changes From 61cf16708b887259c0e930e7d93c996912efdb8a Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 11 Mar 2026 10:36:39 +1100 Subject: [PATCH 157/184] chore: tidy up prune logging --- .github/workflows/prune-attachments.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/prune-attachments.yml b/.github/workflows/prune-attachments.yml index b48e9ac62..4576211eb 100644 --- a/.github/workflows/prune-attachments.yml +++ b/.github/workflows/prune-attachments.yml @@ -92,8 +92,7 @@ jobs: [ -d "$dir" ] && is_older_than "$dir" "$REPORT_CUTOFF" && echo "$dir" >> "$REPORT_LIST" done REPORT_FILE_COUNT=$(while IFS= read -r dir; do find "$dir" -type f; done < "$REPORT_LIST" | wc -l) - echo "($REPORT_FILE_COUNT files total)" >> $GITHUB_STEP_SUMMARY - apply_deletions "$REPORT_LIST" "report directories" + apply_deletions "$REPORT_LIST" "report directories ($REPORT_FILE_COUNT files)" - name: Commit changes if: github.event.inputs.dry-run != 'true' From 57b0fc8a1c0e4f4b1536ff5543782494255ef891 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 12 Mar 2026 10:52:24 +1100 Subject: [PATCH 158/184] chore: opt into Node 24 for GitHub Actions ahead of June 2026 deprecation Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/android-regression.yml | 3 +++ .github/workflows/deploy-gh-pages.yml | 6 +++++- .github/workflows/ios-regression.yml | 3 +++ .github/workflows/pull.yml | 2 +- github/actions/setup/action.yml | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 092b0b4a5..4e2da666e 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -72,6 +72,9 @@ jobs: android-regression: runs-on: [self-hosted, linux, X64, qa-android] env: + # TODO: remove once pnpm/action-setup releases a Node 24 version + # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PLATFORM: 'android' APK_URL: ${{ github.event.inputs.APK_URL }} BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index 4e2831590..cef4ad099 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -22,6 +22,10 @@ concurrency: jobs: deploy: runs-on: ubuntu-latest + env: + # TODO: remove once configure-pages and deploy-pages release Node 24 versions + # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -36,7 +40,7 @@ jobs: uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: '.' diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 734df4e92..c732c9a6f 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -73,6 +73,9 @@ jobs: ios-regression: runs-on: [self-hosted, macOS] env: + # TODO: remove once pnpm/action-setup releases a Node 24 version + # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PLATFORM: 'ios' APK_URL: ${{ github.event.inputs.APK_URL }} BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index f60bc0fe7..0db695afe 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -25,7 +25,7 @@ jobs: submodules: 'recursive' - name: Install node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index c0245021a..cfdcef92a 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -8,7 +8,7 @@ runs: - name: Install pnpm uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' cache: 'pnpm' From 07ff72652e9654c100480065c73e84fea9b6a9d3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Mar 2026 09:36:18 +1100 Subject: [PATCH 159/184] fix: close apps on all devices --- run/test/specs/linked_device_community_ban.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/specs/linked_device_community_ban.spec.ts b/run/test/specs/linked_device_community_ban.spec.ts index c09eb3a30..2725624ac 100644 --- a/run/test/specs/linked_device_community_ban.spec.ts +++ b/run/test/specs/linked_device_community_ban.spec.ts @@ -122,7 +122,7 @@ async function banUnbanLinked(platform: SupportedPlatformsType, testInfo: TestIn ]); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { - await closeApp(bob1, alice1); + await closeApp(alice1, bob1, bob2); }); } @@ -205,6 +205,6 @@ async function banAndDeleteLinked(platform: SupportedPlatformsType, testInfo: Te await alice1.verifyElementNotPresent(new MessageBody(alice1, msg2)); }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { - await closeApp(bob1, alice1); + await closeApp(alice1, bob1, bob2); }); } From e40dc49cc41219d0d959241c7d1bcac631ef7142 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Mar 2026 11:42:18 +1100 Subject: [PATCH 160/184] fix: use `verify()` over `assert()` to avoid ambiguity --- run/test/specs/qr_codes.spec.ts | 4 ++-- run/test/utils/conversation_order.ts | 4 ++-- run/test/utils/index.ts | 4 ++-- run/test/utils/utilities.ts | 8 ++++---- run/types/DeviceWrapper.ts | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/run/test/specs/qr_codes.spec.ts b/run/test/specs/qr_codes.spec.ts index bc9a9f703..a22e20af0 100644 --- a/run/test/specs/qr_codes.spec.ts +++ b/run/test/specs/qr_codes.spec.ts @@ -12,7 +12,7 @@ import { AccountRestoreButton, FastModeRadio } from '../locators/onboarding'; import { RecoveryPasswordMenuItem, UserSettings, ViewQR } from '../locators/settings'; import { JoinCommunityOption, NewMessageOption } from '../locators/start_conversation'; import { open_Alice1_bob1_notfriends } from '../state_builder'; -import { assert, clickOnCoordinates, sleepFor } from '../utils'; +import { clickOnCoordinates, sleepFor, verify } from '../utils'; import { joinCommunity } from '../utils/community'; import { newUser } from '../utils/create_account'; import { truncatePubkey } from '../utils/get_account_id'; @@ -88,7 +88,7 @@ async function qrCodeSeedPhrase(platform: SupportedPlatformsType, testInfo: Test new AccountIDDisplay(device2) ); const secondAccountID = await device2.getTextFromElement(secondAccountIDElement); - assert(firstAccountID, 'The account recovered from QR code is not the right one').toBe( + verify(firstAccountID, 'The account recovered from QR code is not the right one').toBe( secondAccountID ); }); diff --git a/run/test/utils/conversation_order.ts b/run/test/utils/conversation_order.ts index 4292eb6b8..6747aca41 100644 --- a/run/test/utils/conversation_order.ts +++ b/run/test/utils/conversation_order.ts @@ -1,5 +1,5 @@ import { DeviceWrapper } from '../../types/DeviceWrapper'; -import { assert } from './'; +import { verify } from './'; // Returns the names of all conversation list items in their current DOM order export const getConversationOrder = async (device: DeviceWrapper): Promise => { @@ -26,5 +26,5 @@ export const assertPinOrder = ( } const expected = [...pinnedExpected, ...unpinnedExpected]; - assert(afterOrder, 'Conversation order is not correct').toEqual(expected); + verify(afterOrder, 'Conversation order is not correct').toEqual(expected); }; diff --git a/run/test/utils/index.ts b/run/test/utils/index.ts index 277d81b1d..e7140f74a 100644 --- a/run/test/utils/index.ts +++ b/run/test/utils/index.ts @@ -1,6 +1,6 @@ import { clickOnCoordinates } from './click_by_coordinates'; import { runOnlyOnAndroid, runOnlyOnIOS } from './run_on'; import { sleepFor } from './sleep_for'; -import { assert } from './utilities'; +import { verify } from './utilities'; -export { assert, sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; +export { verify, sleepFor, runOnlyOnIOS, runOnlyOnAndroid, clickOnCoordinates }; diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index 2508ce617..dc28f933f 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -156,7 +156,7 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise * Drop-in replacement for Playwright's `expect()` that keeps Allure reports clean. * * Playwright dumps the full diff (received vs expected) into the error message, which - * ends up verbatim in Allure — too technical for customers. `assert()` catches + * ends up verbatim in Allure — too technical for customers. `verify()` catches * assertion errors and rethrows with only the human-readable `message`, preserving the diffs * in the runner logs. * @@ -164,10 +164,10 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise * @param message - Business-readable failure message — this is all Allure will show on failure. * * @example - * assert(messages, 'Conversation messages are in the wrong order').toEqual(expected); - * assert(isVisible, 'Blocked user banner should not be visible').not.toBe(true); + * verify(messages, 'Conversation messages are in the wrong order').toEqual(expected); + * verify(isVisible, 'Blocked user banner should not be visible').not.toBe(true); */ -export function assert(actual: T, message: string) { +export function verify(actual: T, message: string) { const matchers = expect(actual, message); function wrapMatchers(obj: typeof matchers): typeof matchers { diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 0f4fc7ec9..4b3eb4a15 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -73,7 +73,7 @@ import { VersionNumber, } from '../test/locators/settings'; import { EnterAccountID, NewMessageOption, NextButton } from '../test/locators/start_conversation'; -import { assert, clickOnCoordinates, sleepFor } from '../test/utils'; +import { clickOnCoordinates, sleepFor, verify } from '../test/utils'; import { getAdbFullPath } from '../test/utils/binaries'; import { parseDataImage } from '../test/utils/check_colour'; import { isSameColor } from '../test/utils/check_colour'; @@ -1906,7 +1906,7 @@ export class DeviceWrapper { ) { const el = await this.waitForTextElementToBePresent(element); const received = await this.getAttribute(attribute, el.ELEMENT); - assert(received, 'Element attribute value mismatch').toBe(value); + verify(received, 'Element attribute value mismatch').toBe(value); } public async disappearRadioButtonSelected( @@ -2706,7 +2706,7 @@ export class DeviceWrapper { for (let i = 0; i < SAMPLE_SIZE; i++) { colors.add(await this.getElementPixelColor(locator)); } - assert( + verify( colors.size, `Expected element to be animated but detected 1 unique color: ${[...colors][0]}` ).toBeGreaterThan(1); From 6afb39e1a2e3b01a83bdb33a3d21575600038de1 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 17 Mar 2026 13:31:29 +1100 Subject: [PATCH 161/184] chore: update readme --- README.md | 188 +++++++++++++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 8232b6be7..e9912ab2c 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,109 @@ -# Automation testing for Session +# Automated Testing for Session Mobile -This repository holds the code to do integration tests with Appium and Session on iOS and Android. +This repository holds the code to run integration tests for Session iOS and Android. -# Setup - -## Android SDK & Emulators - -First, you need to download android studio at https://developer.android.com/studio. - -Once installed, run it, open the SDK Manager and install the latest SDK tools. - -Once this is done, open up the AVD Manager, click on "Create Device" -> "Pixel 6" -> Next -> Select the System Image you want (I did my tests with **UpsideDownCake**), install it, select it, "Next" and "Finish". - -Then, create a second emulator following the exact same steps (the tests need 2 different emulators to run). - -Once done, you should be able to start each emulators and have them running at the same time. They will need to be running for the tests to work, because Appium won't start them. - -## Environment variables needed - -Before you can start the tests, you need to setup some environment variables. See the file .env.sample for an example. - -#### ANDROID_SDK_ROOT - -`ANDROID_SDK_ROOT` should point to the folder containing the sdks, so the folder containing folders like `platform-tools`, `system-images`, etc... -`export ANDROID_SDK_ROOT=~/Android/Sdk` - -#### APPIUM_ANDROID_BINARIES_ROOT - -`APPIUM_ANDROID_BINARIES_ROOT` should point to the file containing the apks to install for testing (such as `session-1.18.2-x86.apk`) -`export APPIUM_ANDROID_BINARIES_ROOT=~/appium-binaries` - -#### APPIUM_ADB_FULL_PATH - -`APPIUM_ADB_FULL_PATH` should point to the binary of adb inside the ANDROID_SDK folder -`export APPIUM_ADB_FULL_PATH=~/Android/Sdk/platform-tools/adb` - -### Multiple adb binaries - -Having multiple adb on your system will make tests unreliable, because the server will be restarted by Appium. - -On linux, if running `which adb` does not point to the `adb` binary in the `ANDROID_SDK_ROOT` you will have issues. - -You can get rid of adb on linux by running - -``` -sudo apt remove adb -sudo apt remove android-tools-adb -``` - -`which adb` should not return anything. - -Somehow, Appium asks for the sdk tools but do not force the adb binary to come from the sdk tools folder. Making sure that there is no adb in your path should solve this. - -## Running tests on iOS Emulators - -First you need to get correct branch of Session that you want to test from Github. See [(https://github.com/session-foundation/session-ios/releases/)] and download the latest **ipa** under **Assets** - -Then to access the **.app** file that Appium needs for testing you need to build in Xcode and then find .app in your **Derived Data** folder for Xcode. - -For Mac users this file will exist in: - -Macintosh HD > Username > Library > Developer > Xcode > Derived Data > (Then there will be a version of Session with a very long line of letters) > Build > Products > App store-iphonesimulator > Session.app - -Then Copy and Paste then app file onto Desktop (or anywhere you can access easily) then each time you build, navigate back to the file in Derived Data and copy and paste back to Desktop. -Then set the path to Session.app in your ios capabilities file. - -## Appium & tests setup - -First, install nvm for your system (https://github.com/nvm-sh/nvm). -For windows, head here: https://github.com/coreybutler/nvm-windows -For Mac, https://github.com/nvm-sh/nvm +## Quick Start You can check the current node version in `.tool-versions` -``` -nvm install -nvm use -git lfs install -git lfs pull -git submodule update --init --recursive -pnpm install --frozen-lockfile -``` - -Then, choose an option: +1. **Install dependencies:** + ``` + nvm install + nvm use + git submodule update --init --recursive + pnpm install --frozen-lockfile + ``` +2. **Install Git LFS:** + ```bash + # macOS + brew install git-lfs + + # Linux + sudo apt install git-lfs + + # Then + git lfs install + git lfs pull # Ensure all LFS files are downloaded + ``` +3. **Setup environment:** + ```bash + cp .env.sample .env + # Edit .env with your specific paths - see Environment Configuration below + ``` +4. **Run tests locally:** + ```bash + pnpm start-server # Starts Appium server + pnpm test # Run all tests + pnpm test-android # Android tests only + pnpm test-ios # iOS tests only + pnpm test-one 'Test name' # Run specific test (both platforms) + pnpm test-one 'Test name @android' # Run specific test on one platform + ``` + +## Local Development + +Note: The tests use devices with specific resolutions for visual regression testing - ensure you have these available (see below). + +### Android + +Prerequisites: Android Studio installed with SDK tools available +1. Create 4x Pixel 6 emulators via AVD Manager (minimum 4 emulators - tests require up to 4 devices simultaneously) + - Recommended system image is Android API 34 with Google Play services +2. Configure the emulators' virtual scene to enable custom image injection to camera viewport + ```bash + pnpm setup-virtual-scene + ``` +3. Download Session binaries from [the build repository](https://oxen.rocks) + - Choose the appropriate binary based on your network access: + - QA: Pre-configured to mainnet, can run on any network + - AutomaticQA: Pre-configured to a local devnet, must have access +4. Set environment variable: + ```bash + # In your .env file + ANDROID_APK=/path/to/session-android.apk + ``` +5. Start emulators manually - they need to be running before tests start (Appium won't launch them automatically) + ```bash + emulator @ + ``` + +### iOS +Prerequisites: Xcode installed + +1. Create iOS simulators with preloaded media attachments: + ```bash + # Local development (create 4 simulators to be able to run all tests) + pnpm create-simulators 4 + # Or specify custom count + pnpm create-simulators + ``` +2. Download Session binaries from the [the build repository](https://oxen.rocks) +3. Extract .app file and copy Session.app to an easily accessible location +4. Set environment variable: + + ```bash + # In your .env file + IOS_APP_PATH_PREFIX=/path/to/Session.app + ``` + +### Environment Configuration + +Copy `.env.sample` to `.env` and configure the following: + +**Required paths:** +```bash +ANDROID_SDK_ROOT=/path/to/Android/Sdk # SDK tools auto-discovered from here +ANDROID_APK=/path/to/session-android.apk # Android APK for testing +IOS_APP_PATH_PREFIX=/path/to/Session.app # iOS app for testing ``` -pnpm run test # Run all the tests - -Platform specific -pnpm run test-android # To run just Android tests -pnpm run test-ios # To run just iOS tests -pnpm run test-one 'Name of test' # To run one test (on both platforms) -pnpm run test-one 'Name of test @android/@ios' # To run one test on either platform +**Test configuration:** +```bash +_TESTING=1 # Skip printing appium/wdio logs +PLAYWRIGHT_RETRIES_COUNT=0 # Test retry attempts +PLAYWRIGHT_REPEAT_COUNT=0 # Successful test repeat count +PLAYWRIGHT_WORKERS_COUNT=1 # Parallel test workers +CI=0 # Set to 1 to simulate CI (mostly for Allure reporting) +ALLURE_ENABLED='false' # Set to 'true' to generate Allure reports (in conjunction with CI=1) +UPDATE_BASELINES=1 # Auto-save new screenshot baselines if unavailable ``` From e668c332cb36a5895b751bd8e8376957ec865de4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Mar 2026 09:54:21 +1100 Subject: [PATCH 162/184] fix: ios 2.15.0 tweaks --- run/screenshots/ios/settings.png | 4 ++-- run/test/specs/linked_device_unsend_message.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/run/screenshots/ios/settings.png b/run/screenshots/ios/settings.png index bc61e279d..6617e2b8a 100644 --- a/run/screenshots/ios/settings.png +++ b/run/screenshots/ios/settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75f75aae9c68a9408622dac844f0a2d98541c8fa938156d85833187ea36b1448 -size 199765 +oid sha256:6de0b55dd8591b86184b0595db0d0936227201de59e1678a590e9378de24d58b +size 194857 diff --git a/run/test/specs/linked_device_unsend_message.spec.ts b/run/test/specs/linked_device_unsend_message.spec.ts index 2aabc0ce1..ef20acc41 100644 --- a/run/test/specs/linked_device_unsend_message.spec.ts +++ b/run/test/specs/linked_device_unsend_message.spec.ts @@ -69,11 +69,11 @@ async function unSendMessageLinkedDevice(platform: SupportedPlatformsType, testI await Promise.all( [alice1, bob1, alice2].map(async device => { await device.waitForTextElementToBePresent(new MessageBody(device, firstMessage)); - await device.verifyElementNotPresent(new MessageBody(device, secondMessage)); await device.waitForTextElementToBePresent({ ...new DeletedMessage(device).build(), maxWait: 10_000, }); + await device.verifyElementNotPresent(new MessageBody(device, secondMessage)); await device.back(); }) ); From 7ef45b2f0284d199729319e0d3633331b3a75b5b Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Mar 2026 14:05:01 +1100 Subject: [PATCH 163/184] feat: add `clickAndWaitFor` method --- run/test/utils/set_disappearing_messages.ts | 5 +++- run/types/DeviceWrapper.ts | 30 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/run/test/utils/set_disappearing_messages.ts b/run/test/utils/set_disappearing_messages.ts index 29683962a..41ae9a26c 100644 --- a/run/test/utils/set_disappearing_messages.ts +++ b/run/test/utils/set_disappearing_messages.ts @@ -14,7 +14,10 @@ export const setDisappearingMessage = async ( [conversationType, timerType, timerDuration = DISAPPEARING_TIMES.THIRTY_SECONDS]: MergedOptions ) => { const enforcedType: ConversationType = conversationType; - await device.clickOnElementAll(new ConversationSettings(device)); + await device.clickAndWaitFor( + new ConversationSettings(device), + new DisappearingMessagesMenuOption(device) + ); await device.clickOnElementAll(new DisappearingMessagesMenuOption(device)); if (enforcedType === '1:1') { await device.clickOnElementAll(new DisappearingMessagesTimerType(device, timerType)); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 4b3eb4a15..82d01a0f8 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -720,6 +720,36 @@ export class DeviceWrapper { return el; } + /** + * Clicks an element and retries until an expected element appears, confirming the click registered. + * Useful for flaky taps where Appium reports success but the UI doesn't respond. + * + * @param args - The element to click + * @param waitFor - A locator that should become present after a successful click + */ + public async clickAndWaitFor( + args: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj), + waitFor: { text?: string; maxWait?: number } & (LocatorsInterface | StrategyExtractionObj) + ) { + const { description: firstLocator } = this.resolveLocator(args); + const { locator: waitForLocator } = this.resolveLocator(waitFor); + + return this.pollUntil( + async () => { + const el = await this.waitForTextElementToBePresent(args); + await this.click(el.ELEMENT); + try { + await this.waitForTextElementToBePresent({ ...waitForLocator, maxWait: 1_000 }); + return { success: true, data: el }; + } catch { + this.log(`Click on ${firstLocator} did not produce expected result, retrying...`); + return { success: false, error: `Click on ${firstLocator} did not produce expected result` }; + } + }, + { maxWait: 5_000, pollInterval: 500 } + ); + } + public async clickOnElementByText( args: { text: string; maxWait?: number } & StrategyExtractionObj ) { From d1e61d5f4438f55f9cfa7132cf104f86be371430 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 19 Mar 2026 14:09:31 +1100 Subject: [PATCH 164/184] chore: linting --- run/types/DeviceWrapper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 82d01a0f8..2187fbeda 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -743,7 +743,10 @@ export class DeviceWrapper { return { success: true, data: el }; } catch { this.log(`Click on ${firstLocator} did not produce expected result, retrying...`); - return { success: false, error: `Click on ${firstLocator} did not produce expected result` }; + return { + success: false, + error: `Click on ${firstLocator} did not produce expected result`, + }; } }, { maxWait: 5_000, pollInterval: 500 } From b054c12a6b0312fa83a59f0fbc1fdc29de9b76f7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Mar 2026 10:24:03 +1100 Subject: [PATCH 165/184] chore: update strings --- .gitmodules | 3 ++- run/localizer/lib | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index e5d68dc1e..3a7ca6af5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "run/localizer/lib"] path = run/localizer/lib - url = https://github.com/session-foundation/session-localization.git + branch = main + url = https://github.com/session-foundation/session-localization.git \ No newline at end of file diff --git a/run/localizer/lib b/run/localizer/lib index f2620de9c..94eb07814 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit f2620de9c8dc757ae7a131c55f60cdfd0074f47f +Subproject commit 94eb078144540390d4209d805c5373be0ccba33c From 9a0395456c36edd79d89e6bc4a167c464e6521e8 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Mar 2026 11:43:34 +1100 Subject: [PATCH 166/184] fix: update tests for donate appeal --- run/screenshots/android/cta_donate.png | 4 +-- run/screenshots/ios/cta_donate.png | 4 +-- run/test/specs/cta_donate_review.spec.ts | 10 ------- run/test/specs/donate.spec.ts | 10 +++---- run/types/DeviceWrapper.ts | 34 ++++++++++++------------ run/types/cta.ts | 24 ++++++++--------- 6 files changed, 36 insertions(+), 50 deletions(-) diff --git a/run/screenshots/android/cta_donate.png b/run/screenshots/android/cta_donate.png index 704211b7c..02f166a55 100644 --- a/run/screenshots/android/cta_donate.png +++ b/run/screenshots/android/cta_donate.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3975c78efa3c6c09a91fc78f8d82db5d4d48a2fe1c2f11c681f11bb4c03eef07 -size 1115997 +oid sha256:9ad60a2e05dddf51445ebd7ecee2bc55397b77ef4a5a9630254146eb2b452d58 +size 731877 diff --git a/run/screenshots/ios/cta_donate.png b/run/screenshots/ios/cta_donate.png index 3f5c823d3..5d22106be 100644 --- a/run/screenshots/ios/cta_donate.png +++ b/run/screenshots/ios/cta_donate.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b37a27b043bdcb46cb529411b4e28ac2f5c064fad123888278d67a7599c7d719 -size 2262335 +oid sha256:d1430c35b3084b712251e5c1e5a208e2e861fde023d4356fd330e30ac7a567f8 +size 1694886 diff --git a/run/test/specs/cta_donate_review.spec.ts b/run/test/specs/cta_donate_review.spec.ts index 267cfa95b..39971cb75 100644 --- a/run/test/specs/cta_donate_review.spec.ts +++ b/run/test/specs/cta_donate_review.spec.ts @@ -1,11 +1,9 @@ import test, { TestInfo } from '@playwright/test'; -import { tStripped } from '../../localizer/lib'; import { TestSteps } from '../../types/allure'; import { bothPlatformsIt } from '../../types/sessionIt'; import { USERNAME } from '../../types/testing'; import { CloseSettings } from '../locators'; -import { CTAButtonPositive } from '../locators/global'; import { ReviewPromptItsGreatButton } from '../locators/home'; import { PathMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; @@ -26,7 +24,6 @@ bothPlatformsIt({ }); async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestInfo) { - const donateURL = 'https://getsession.org/donate#app'; const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); await newUser(device, USERNAME.ALICE, { saveUserData: false }); @@ -50,13 +47,6 @@ async function donateCTAReview(platform: SupportedPlatformsType, testInfo: TestI await test.step(TestSteps.VERIFY.SCREENSHOT('Donate CTA'), async () => { await verifyPageScreenshot(device, platform, 'cta_donate', testInfo); }); - await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Open URL'), async () => { - await device.clickOnElementAll(new CTAButtonPositive(device)); - await device.checkModalStrings( - tStripped('urlOpen'), - tStripped('urlOpenDescription', { url: donateURL }) - ); - }); await test.step(TestSteps.SETUP.CLOSE_APP, async () => { await closeApp(device); }); diff --git a/run/test/specs/donate.spec.ts b/run/test/specs/donate.spec.ts index 31bda2eea..65ae1ea41 100644 --- a/run/test/specs/donate.spec.ts +++ b/run/test/specs/donate.spec.ts @@ -9,7 +9,7 @@ import { DonationsMenuItem, UserSettings } from '../locators/settings'; import { newUser } from '../utils/create_account'; import { handleChromeFirstTimeOpen } from '../utils/handle_first_open'; import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; -import { assertUrlIsReachable, ensureHttpsURL } from '../utils/utilities'; +import { assertUrlIsReachable, ensureHttpsURL, verify } from '../utils/utilities'; bothPlatformsIt({ title: 'Donate Settings menu item', @@ -25,7 +25,7 @@ bothPlatformsIt({ async function donateLinkout(platform: SupportedPlatformsType, testInfo: TestInfo) { const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); - const linkURL = 'https://getsession.org/donate#app'; + const linkURL = 'https://getsession.org/donate'; await newUser(device, USERNAME.ALICE, { saveUserData: false }); await device.clickOnElementAll(new UserSettings(device)); await device.clickOnElementAll(new DonationsMenuItem(device)); @@ -45,11 +45,7 @@ async function donateLinkout(platform: SupportedPlatformsType, testInfo: TestInf const actualUrlField = await device.getTextFromElement(urlField); const fullRetrievedURL = ensureHttpsURL(actualUrlField); // Verify that it's the correct URL - if (fullRetrievedURL !== linkURL) { - throw new Error( - `The retrieved URL does not match the expected. The retrieved URL is ${fullRetrievedURL}` - ); - } + verify(fullRetrievedURL, 'The retrieved URL does not match the expected').toBe(linkURL); await assertUrlIsReachable(linkURL); // Close browser and app await device.backToSession(); diff --git a/run/types/DeviceWrapper.ts b/run/types/DeviceWrapper.ts index 2187fbeda..aa266c6f7 100644 --- a/run/types/DeviceWrapper.ts +++ b/run/types/DeviceWrapper.ts @@ -2643,14 +2643,16 @@ export class DeviceWrapper { this.assertTextMatches(actualDescription, expectedDescription, 'Modal description'); } - private async checkCTAStrings({ heading, body, buttons, features }: CTAConfig): Promise { - // Validate input + private async checkCTAStrings({ + heading, + body, + negativeButton, + positiveButton, + features, + }: CTAConfig): Promise { if (features && features.length > 3) { throw new Error('CTAs support maximum 3 features'); } - if (buttons.length < 1 || buttons.length > 2) { - throw new Error('CTAs must have 1-2 buttons'); - } // CTA heading const elHeading = await this.waitForTextElementToBePresent(new CTAHeading(this)); @@ -2676,18 +2678,16 @@ export class DeviceWrapper { } } - /** - * buttons[0] = negative/dismiss (always present); - * buttons[1] = positive/action (optional) - */ - const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); - const actualNegative = await this.getTextFromElement(elNegative); - this.assertTextMatches(actualNegative, buttons[0], 'CTA negative button'); + if (negativeButton) { + const elNegative = await this.waitForTextElementToBePresent(new CTAButtonNegative(this)); + const actualNegative = await this.getTextFromElement(elNegative); + this.assertTextMatches(actualNegative, negativeButton, 'CTA negative button'); + } - if (buttons.length === 2) { + if (positiveButton) { const elPositive = await this.waitForTextElementToBePresent(new CTAButtonPositive(this)); const actualPositive = await this.getTextFromElement(elPositive); - this.assertTextMatches(actualPositive, buttons[1], 'CTA positive button'); + this.assertTextMatches(actualPositive, positiveButton, 'CTA positive button'); } } @@ -2695,24 +2695,24 @@ export class DeviceWrapper { await this.checkCTAStrings(ctaConfigs[type]); } - // This is the bare minimum of a CTA so we only check these public async verifyNoCTAShows(): Promise { await Promise.all([ this.verifyElementNotPresent(new CTAHeading(this)), this.verifyElementNotPresent(new CTABody(this)), this.verifyElementNotPresent(new CTAButtonNegative(this)), + this.verifyElementNotPresent(new CTAButtonPositive(this)), ]); } // Dismiss any CTA if it shows public async dismissCTA(): Promise { const hasCTAAppeared = await this.doesElementExist({ - ...new CTAButtonNegative(this).build(), + ...new CTAHeading(this).build(), maxWait: 8_000, }); if (hasCTAAppeared) { this.log('Dismissing CTA'); - await this.clickOnElementAll(new CTAButtonNegative(this)); + await this.clickOnCoordinates(150, 150); } } diff --git a/run/types/cta.ts b/run/types/cta.ts index ecdecc76a..acac9cb4c 100644 --- a/run/types/cta.ts +++ b/run/types/cta.ts @@ -7,27 +7,25 @@ export type CTAType = | 'longerMessages' | 'pinnedConversations'; -/** - * buttons[0] is the negative/dismiss button (always present); - * buttons[1] is the optional positive/action button - */ export type CTAConfig = { heading: string; body: string; - buttons: [string, string] | [string]; + negativeButton?: string; + positiveButton?: string; features?: string[]; }; export const ctaConfigs: Record = { donate: { - heading: tStripped('donateSessionHelp'), - body: tStripped('donateSessionDescription'), - buttons: [tStripped('maybeLater'), tStripped('donate')], + heading: tStripped('donateSessionAppealTitle'), + body: tStripped('donateSessionAppealDescription'), + positiveButton: tStripped('donateSessionAppealReadMore'), }, longerMessages: { heading: tStripped('upgradeTo'), body: tStripped('proCallToActionLongerMessages'), - buttons: [tStripped('cancel'), tStripped('theContinue')], + negativeButton: tStripped('cancel'), + positiveButton: tStripped('theContinue'), features: [ tStripped('proFeatureListLongerMessages'), tStripped('proFeatureListPinnedConversations'), @@ -37,7 +35,8 @@ export const ctaConfigs: Record = { animatedProfilePicture: { heading: tStripped('upgradeTo'), body: tStripped('proAnimatedDisplayPictureCallToActionDescription'), - buttons: [tStripped('cancel'), tStripped('theContinue')], + negativeButton: tStripped('cancel'), + positiveButton: tStripped('theContinue'), features: [ tStripped('proFeatureListAnimatedDisplayPicture'), tStripped('proFeatureListLongerMessages'), @@ -47,12 +46,13 @@ export const ctaConfigs: Record = { alreadyActivated: { heading: tStripped('proActivated'), body: tStripped('proAnimatedDisplayPicture'), - buttons: [tStripped('close')], + negativeButton: tStripped('close'), }, pinnedConversations: { heading: tStripped('upgradeTo'), body: tStripped('proCallToActionPinnedConversationsMoreThan', { limit: '5' }), - buttons: [tStripped('cancel'), tStripped('theContinue')], + negativeButton: tStripped('cancel'), + positiveButton: tStripped('theContinue'), features: [ tStripped('proFeatureListPinnedConversations'), tStripped('proFeatureListLongerMessages'), From f07a54b6e239abea4190573904eaf2a1275902d3 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Mar 2026 13:10:41 +1100 Subject: [PATCH 167/184] fix: throw expected/actual in allure msg --- run/test/utils/utilities.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index dc28f933f..6a41ad7fe 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -153,15 +153,15 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise } /** - * Drop-in replacement for Playwright's `expect()` that keeps Allure reports clean. + * Wrapper for Playwright's `expect()` that keeps Allure reports clean. * - * Playwright dumps the full diff (received vs expected) into the error message, which - * ends up verbatim in Allure — too technical for customers. `verify()` catches - * assertion errors and rethrows with only the human-readable `message`, preserving the diffs - * in the runner logs. + * Playwright dumps the raw diff into the error message, + * which can be confusing for report readers. + * + * `verify()` catches assertion errors and rethrows with a clean message. * * @param actual - The value being asserted - * @param message - Business-readable failure message — this is all Allure will show on failure. + * @param message - Business-readable failure message for reporting * * @example * verify(messages, 'Conversation messages are in the wrong order').toEqual(expected); @@ -178,18 +178,24 @@ export function verify(actual: T, message: string) { return wrapMatchers(val as typeof matchers); if (typeof val === 'function') { return (...args: unknown[]) => { + const mismatch = () => { + const lines = [message]; + if (args.length > 0) { + lines.push(`Expected: ${String(args[0])}`); + lines.push(`Actual: ${String(actual)}`); + } + return new Error(lines.join('\n')); + }; try { const result = (val as (...a: unknown[]) => unknown).apply(target, args); if (result instanceof Promise) { return result.catch(() => { - console.log(`${message}\n actual: `, actual, '\n expected:', args[0]); - throw new Error(message); + throw mismatch(); }); } return result; } catch { - console.log(`${message}\n actual: `, actual, '\n expected:', args[0]); - throw new Error(message); + throw mismatch(); } }; } From 06383e7313cd49ea18da4793711f4ca9e8254f6d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Fri, 20 Mar 2026 13:11:21 +1100 Subject: [PATCH 168/184] chore: linting --- run/test/utils/utilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run/test/utils/utilities.ts b/run/test/utils/utilities.ts index 6a41ad7fe..7971ed384 100644 --- a/run/test/utils/utilities.ts +++ b/run/test/utils/utilities.ts @@ -155,9 +155,9 @@ export async function forceStopAndRestart(device: DeviceWrapper): Promise /** * Wrapper for Playwright's `expect()` that keeps Allure reports clean. * - * Playwright dumps the raw diff into the error message, + * Playwright dumps the raw diff into the error message, * which can be confusing for report readers. - * + * * `verify()` catches assertion errors and rethrows with a clean message. * * @param actual - The value being asserted From ad031e3b71c8b6a83793fdf3a9a40a1b089410cf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Mar 2026 15:13:50 +1100 Subject: [PATCH 169/184] chore: update strings --- run/localizer/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/localizer/lib b/run/localizer/lib index 94eb07814..cb321331c 160000 --- a/run/localizer/lib +++ b/run/localizer/lib @@ -1 +1 @@ -Subproject commit 94eb078144540390d4209d805c5373be0ccba33c +Subproject commit cb321331ce4f258da82aff0002387c1812175258 From b474946c6eff20af2502eae44f570e8f2c499507 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Mar 2026 15:41:22 +1100 Subject: [PATCH 170/184] fix: latest android dev changes --- run/screenshots/android/settings_notifications.png | 4 ++-- run/test/specs/message_community_invitation.spec.ts | 3 ++- run/test/utils/handle_first_open.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/run/screenshots/android/settings_notifications.png b/run/screenshots/android/settings_notifications.png index 6229c00a9..01c7e0578 100644 --- a/run/screenshots/android/settings_notifications.png +++ b/run/screenshots/android/settings_notifications.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aec60ca8cc003a4d5e906a0b4c6a28e2b31c64ebfb5a601c998d5c22d3727c18 -size 147698 +oid sha256:a7dd7ac62c92ad833db6e916fb7fef1f2abb7e2a08905d8c909accaa47949b69 +size 189299 diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index 20de78138..e4100c7ab 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -43,9 +43,10 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf await alice1.clickOnElementAll(new CommunityInviteConfirmButton(alice1)); await bob1.waitForTextElementToBePresent(new CommunityInvitation(bob1)); await bob1.clickOnElementAll(new CommunityInvitation(bob1)); + const joinCommunityModaBody = platform === 'android' ? tStripped('joinThisCommunity') : tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }) await bob1.checkModalStrings( tStripped('communityJoin'), - tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }) + joinCommunityModaBody ); await bob1.clickOnElementAll(new JoinCommunityModalButton(bob1)); await bob1.navigateBack(); diff --git a/run/test/utils/handle_first_open.ts b/run/test/utils/handle_first_open.ts index 507e966a8..aee80a8bc 100644 --- a/run/test/utils/handle_first_open.ts +++ b/run/test/utils/handle_first_open.ts @@ -6,7 +6,7 @@ import { iOSPhotosContinuebutton } from '../locators/external'; export async function handleChromeFirstTimeOpen(device: DeviceWrapper) { const chromeUseWithoutAnAccount = await device.doesElementExist({ ...new ChromeUseWithoutAnAccount(device).build(), - maxWait: 2_000, + maxWait: 5_000, }); if (!chromeUseWithoutAnAccount) { device.log('Chrome opened without an account check, proceeding'); From 09071954ec1c8611d1a51313e44590b892c86fed Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 24 Mar 2026 19:00:18 +1100 Subject: [PATCH 171/184] chore: new screenshots --- run/screenshots/android/conversation_alice.png | 4 ++-- run/screenshots/android/conversation_bob.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/run/screenshots/android/conversation_alice.png b/run/screenshots/android/conversation_alice.png index 0ee18c700..52a0f131b 100644 --- a/run/screenshots/android/conversation_alice.png +++ b/run/screenshots/android/conversation_alice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:075707255598a95252744a10f63b009aab26f51261fd1886b30bfe8ec37e8873 -size 88351 +oid sha256:bea9e1f602d30a64762d769bb2dceef500c936393390b21ad6125772c9637404 +size 86966 diff --git a/run/screenshots/android/conversation_bob.png b/run/screenshots/android/conversation_bob.png index cb618ed78..e7e816180 100644 --- a/run/screenshots/android/conversation_bob.png +++ b/run/screenshots/android/conversation_bob.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f4197c0c9eea91fcbc5bdf200a18b6b3701a2d5e78ed6b8f22aebaa696fb43b -size 93516 +oid sha256:91ab543ac7f08b23d4073ac4b5c42f7f930eb6792518b3b433b22ea871ad7ee8 +size 94022 From 3bc646983cc30f483d3499c72cc442f9039f9684 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 26 Mar 2026 17:06:59 +1100 Subject: [PATCH 172/184] feat: community url tests --- run/screenshots/android/upm_home.png | 4 +- run/test/specs/community_links.spec.ts | 200 ++++++++++++++++++ .../message_community_invitation.spec.ts | 27 ++- 3 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 run/test/specs/community_links.spec.ts diff --git a/run/screenshots/android/upm_home.png b/run/screenshots/android/upm_home.png index 05138c368..579ddc6d0 100644 --- a/run/screenshots/android/upm_home.png +++ b/run/screenshots/android/upm_home.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a13186f1e5cec7e4d9507b81b48dada7237609c570713490582269f039fc1b09 -size 103795 +oid sha256:8be4b42a49a946a48c0f1b9885ab24438d3bc0263d994ef0f937fb727f9d9fa2 +size 104018 diff --git a/run/test/specs/community_links.spec.ts b/run/test/specs/community_links.spec.ts new file mode 100644 index 000000000..ed4721030 --- /dev/null +++ b/run/test/specs/community_links.spec.ts @@ -0,0 +1,200 @@ +import { test, TestInfo } from '@playwright/test'; +import { USERNAME } from '@session-foundation/qa-seeder'; + +import { communities } from '../../constants/community'; +import { tStripped } from '../../localizer/lib'; +import { TestSteps } from '../../types/allure'; +import { androidIt } from '../../types/sessionIt'; +import { JoinCommunityModalButton } from '../locators'; +import { ConversationHeaderName, MessageBody } from '../locators/conversation'; +import { CreateGroupButton, GroupNameInput } from '../locators/groups'; +import { PlusButton } from '../locators/home'; +import { + CreateGroupOption, + EnterAccountID, + NewMessageOption, + NextButton, +} from '../locators/start_conversation'; +import { joinCommunity } from '../utils/community'; +import { newUser } from '../utils/create_account'; +import { closeApp, openAppOnPlatformSingleDevice, SupportedPlatformsType } from '../utils/open_app'; + +androidIt({ + title: 'Community URL on New Message - not member', + risk: 'low', + countOfDevicesNeeded: 1, + testCb: communityURLNewConvo, + allureSuites: { + parent: 'New Conversation', + suite: 'Join Community', + }, +}); + +androidIt({ + title: 'Join Community URL on Create Group - not member', + risk: 'low', + countOfDevicesNeeded: 1, + testCb: communityURLGroup, + allureSuites: { + parent: 'New Conversation', + suite: 'Join Community', + }, +}); + +androidIt({ + title: 'Community URL on New Message - member', + risk: 'low', + countOfDevicesNeeded: 1, + testCb: communityURLNewConvoMember, + allureSuites: { + parent: 'New Conversation', + suite: 'Join Community', + }, +}); + +androidIt({ + title: 'Join Community URL on Create Group - member', + risk: 'low', + countOfDevicesNeeded: 1, + testCb: communityURLGroupMember, + allureSuites: { + parent: 'New Conversation', + suite: 'Join Community', + }, +}); + +async function communityURLNewConvo(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Type Community URL in Create Group screen', async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(communities.testCommunity.link, new EnterAccountID(device)); + await device.clickOnElementAll(new NextButton(device)); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Join Community'), async () => { + await device.checkModalStrings( + tStripped('communityJoin'), + tStripped('communityUrlJoinEntered') + ); + }); + await test.step('Verify Community can be joined', async () => { + await device.clickOnElementAll(new JoinCommunityModalButton(device)); + await device.waitForTextElementToBePresent( + new ConversationHeaderName(device, communities.testCommunity.name) + ); + await device.waitForTextElementToBePresent(new MessageBody(device)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function communityURLGroup(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step('Type Community URL in New Message screen', async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new CreateGroupOption(device)); + await device.inputText(communities.testCommunity.link, new GroupNameInput(device)); + await device.clickOnElementAll(new CreateGroupButton(device)); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Join Community'), async () => { + await device.checkModalStrings( + tStripped('communityJoin'), + tStripped('groupNameContainedUrlJoinCommunity') + ); + }); + await test.step('Verify Community can be joined', async () => { + await device.clickOnElementAll(new JoinCommunityModalButton(device)); + await device.waitForTextElementToBePresent( + new ConversationHeaderName(device, communities.testCommunity.name) + ); + await device.waitForTextElementToBePresent(new MessageBody(device)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function communityURLNewConvoMember(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); + await device.navigateBack(); + }); + await test.step('Type Community URL in Create Group screen', async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new NewMessageOption(device)); + await device.inputText(communities.testCommunity.link, new EnterAccountID(device)); + await device.clickOnElementAll(new NextButton(device)); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Join Community'), async () => { + await device.checkModalStrings( + tStripped('openCommunity'), + tStripped('communityUrlOpenEntered', { community_name: communities.testCommunity.name }) + ); + }); + await test.step('Verify Community can be opened', async () => { + await device.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("Open")`, + }); + await device.waitForTextElementToBePresent( + new ConversationHeaderName(device, communities.testCommunity.name) + ); + await device.waitForTextElementToBePresent(new MessageBody(device)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} + +async function communityURLGroupMember(platform: SupportedPlatformsType, testInfo: TestInfo) { + const { device } = await test.step(TestSteps.SETUP.NEW_USER, async () => { + const { device } = await openAppOnPlatformSingleDevice(platform, testInfo); + await newUser(device, USERNAME.ALICE, { saveUserData: false }); + return { device }; + }); + await test.step(TestSteps.NEW_CONVERSATION.JOIN_COMMUNITY, async () => { + await joinCommunity(device, communities.testCommunity.link, communities.testCommunity.name); + await device.navigateBack(); + }); + await test.step('Type Community URL in New Message screen', async () => { + await device.clickOnElementAll(new PlusButton(device)); + await device.clickOnElementAll(new CreateGroupOption(device)); + await device.inputText(communities.testCommunity.link, new GroupNameInput(device)); + await device.clickOnElementAll(new CreateGroupButton(device)); + }); + await test.step(TestSteps.VERIFY.SPECIFIC_MODAL('Join Community'), async () => { + await device.checkModalStrings( + tStripped('openCommunity'), + tStripped('groupNameContainedUrlOpenCommunity', { + community_name: communities.testCommunity.name, + }) + ); + }); + await test.step('Verify Community can be opened', async () => { + await device.clickOnElementAll({ + strategy: '-android uiautomator', + selector: `new UiSelector().text("Open")`, + }); + await device.waitForTextElementToBePresent( + new ConversationHeaderName(device, communities.testCommunity.name) + ); + await device.waitForTextElementToBePresent(new MessageBody(device)); + }); + await test.step(TestSteps.SETUP.CLOSE_APP, async () => { + await closeApp(device); + }); +} diff --git a/run/test/specs/message_community_invitation.spec.ts b/run/test/specs/message_community_invitation.spec.ts index e4100c7ab..95d5c1142 100644 --- a/run/test/specs/message_community_invitation.spec.ts +++ b/run/test/specs/message_community_invitation.spec.ts @@ -7,7 +7,9 @@ import { InviteContactsMenuItem, JoinCommunityModalButton } from '../locators'; import { CommunityInvitation, CommunityInviteConfirmButton, + ConversationHeaderName, ConversationSettings, + MessageBody, } from '../locators/conversation'; import { GroupMember } from '../locators/groups'; import { ConversationItem } from '../locators/home'; @@ -43,15 +45,22 @@ async function sendCommunityInvitation(platform: SupportedPlatformsType, testInf await alice1.clickOnElementAll(new CommunityInviteConfirmButton(alice1)); await bob1.waitForTextElementToBePresent(new CommunityInvitation(bob1)); await bob1.clickOnElementAll(new CommunityInvitation(bob1)); - const joinCommunityModaBody = platform === 'android' ? tStripped('joinThisCommunity') : tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }) - await bob1.checkModalStrings( - tStripped('communityJoin'), - joinCommunityModaBody - ); + const joinCommunityModaBody = + platform === 'android' + ? tStripped('joinThisCommunity') + : tStripped('communityJoinDescription', { community_name: communities.testCommunity.name }); + await bob1.checkModalStrings(tStripped('communityJoin'), joinCommunityModaBody); await bob1.clickOnElementAll(new JoinCommunityModalButton(bob1)); - await bob1.navigateBack(); - await bob1.waitForTextElementToBePresent( - new ConversationItem(bob1, communities.testCommunity.name) - ); + if (platform === 'android') { + await bob1.waitForTextElementToBePresent( + new ConversationHeaderName(bob1, communities.testCommunity.name) + ); + await bob1.waitForTextElementToBePresent(new MessageBody(bob1)); + } else { + await bob1.navigateBack(); + await bob1.waitForTextElementToBePresent( + new ConversationItem(bob1, communities.testCommunity.name) + ); + } await closeApp(alice1, bob1); } From 2375e55516827b0ead93906b499f8b9101d8f70d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 11:29:52 +1100 Subject: [PATCH 173/184] chore: tidy up readme --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e9912ab2c..da29cfd6e 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,17 @@ This repository holds the code to run integration tests for Session iOS and Andr ## Quick Start -You can check the current node version in `.tool-versions` +You can check the current node version in `.tool-versions` or `.nvmrc`. 1. **Install dependencies:** - ``` + ```bash nvm install nvm use - git submodule update --init --recursive - pnpm install --frozen-lockfile + corepack enable + # pnpm is installed by corepack, no separate install required + pnpm install --frozen-lockfile ``` -2. **Install Git LFS:** +2. **Git:** ```bash # macOS brew install git-lfs @@ -23,14 +24,13 @@ You can check the current node version in `.tool-versions` # Then git lfs install - git lfs pull # Ensure all LFS files are downloaded - ``` -3. **Setup environment:** - ```bash - cp .env.sample .env - # Edit .env with your specific paths - see Environment Configuration below + git lfs pull + + # Finally, pull the app strings from the localization repo + git submodule update --init --recursive ``` -4. **Run tests locally:** + +3. **Run tests locally:** ```bash pnpm start-server # Starts Appium server pnpm test # Run all tests @@ -49,6 +49,7 @@ Note: The tests use devices with specific resolutions for visual regression test Prerequisites: Android Studio installed with SDK tools available 1. Create 4x Pixel 6 emulators via AVD Manager (minimum 4 emulators - tests require up to 4 devices simultaneously) - Recommended system image is Android API 34 with Google Play services + - Emulator names are not significant. The tests discover running emulators automatically. 2. Configure the emulators' virtual scene to enable custom image injection to camera viewport ```bash pnpm setup-virtual-scene @@ -68,7 +69,7 @@ Prerequisites: Android Studio installed with SDK tools available ``` ### iOS -Prerequisites: Xcode installed +Prerequisites: Xcode installed and the appropriate simulator runtime available - check in `scripts/create_ios_simulators.ts` 1. Create iOS simulators with preloaded media attachments: ```bash @@ -88,7 +89,9 @@ Prerequisites: Xcode installed ### Environment Configuration -Copy `.env.sample` to `.env` and configure the following: +```bash +cp .env.sample .env +``` **Required paths:** ```bash @@ -106,4 +109,5 @@ PLAYWRIGHT_WORKERS_COUNT=1 # Parallel test workers CI=0 # Set to 1 to simulate CI (mostly for Allure reporting) ALLURE_ENABLED='false' # Set to 'true' to generate Allure reports (in conjunction with CI=1) UPDATE_BASELINES=1 # Auto-save new screenshot baselines if unavailable +SOGS_ADMIN_SEED='word1 word2...' # 13-word recovery phrase of an account that's an admin in the testing SOGS. ``` From 54cfeeb1e2b0aec4e1b8dedb8bb83a6e29f8e10c Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 13:45:06 +1100 Subject: [PATCH 174/184] chore: simplify readme even more --- README.md | 53 ++++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index da29cfd6e..e14f8bf8c 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,28 @@ This repository holds the code to run integration tests for Session iOS and Andr ## Quick Start -You can check the current node version in `.tool-versions` or `.nvmrc`. +### Prerequisites -1. **Install dependencies:** - ```bash - nvm install - nvm use - corepack enable - # pnpm is installed by corepack, no separate install required - pnpm install --frozen-lockfile - ``` -2. **Git:** - ```bash - # macOS - brew install git-lfs - - # Linux - sudo apt install git-lfs - - # Then - git lfs install - git lfs pull - - # Finally, pull the app strings from the localization repo - git submodule update --init --recursive - ``` +- Node.js 24.12.0 +- pnpm 10.28.1 +- Git LFS -3. **Run tests locally:** - ```bash - pnpm start-server # Starts Appium server - pnpm test # Run all tests - pnpm test-android # Android tests only - pnpm test-ios # iOS tests only - pnpm test-one 'Test name' # Run specific test (both platforms) - pnpm test-one 'Test name @android' # Run specific test on one platform - ``` +```bash +pnpm install --frozen-lockfile +git lfs install && git lfs pull +git submodule update --init --recursive +``` + +### Running tests + +```bash +pnpm start-server # Starts Appium server +pnpm test # Run all tests +pnpm test-android # Android tests only +pnpm test-ios # iOS tests only +pnpm test-one 'Test name' # Run specific test (both platforms) +pnpm test-one 'Test name @android' # Run specific test on one platform +``` ## Local Development From cae83adcdeb17c4cb7858af35d5282a0d0dafc80 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 14:02:41 +1100 Subject: [PATCH 175/184] fix copilot comments --- run/test/utils/community.ts | 2 +- run/test/utils/device_registry.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index f9f539c2b..08d10334e 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -1,4 +1,4 @@ -import test from '@playwright/test'; +import { test } from '@playwright/test'; import { communities } from '../../constants/community'; import { DeviceWrapper } from '../../types/DeviceWrapper'; diff --git a/run/test/utils/device_registry.ts b/run/test/utils/device_registry.ts index 5d8ba7f49..4a7c731ce 100644 --- a/run/test/utils/device_registry.ts +++ b/run/test/utils/device_registry.ts @@ -20,8 +20,8 @@ export type DeviceContext = { export const deviceRegistry = new Map(); -export function registryKey(testInfo: TestInfo): string { - return `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}`; +export function registryKey(testInfo: TestInfo, retry = testInfo.retry): string { + return `${testInfo.testId}-${testInfo.parallelIndex}-${testInfo.repeatEachIndex}-${retry}`; } // Async because Android registration fetches per-device PID for scoped logcat on failure. @@ -59,5 +59,8 @@ export async function registerDevicesForTest( } export function unregisterDevicesForTest(testInfo: TestInfo) { - deviceRegistry.delete(registryKey(testInfo)); + // Clean up current attempt and any stale entries left by prior retry attempts + for (let r = 0; r <= testInfo.retry; r++) { + deviceRegistry.delete(registryKey(testInfo, r)); + } } From 83403b17f8960aa9e3acbee19fdf41759f1c9231 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:02:14 +1100 Subject: [PATCH 176/184] chore: add architecture.md --- ARCHITECTURE.md | 178 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 8 ++- 2 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..29eb4ef5a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,178 @@ +# Architecture + +For local setup and running tests, see README.md. + +## Repository Structure + +``` +.github/workflows/ # CI workflows (see Workflows section below) +run/ + test/ + specs/ # Test files (*.spec.ts) + locators/ # UI element locators (LocatorsInterface subclasses + index) + utils/ # Helpers: device open, account creation, screenshots, etc. + state_builder/ # Pre-built multi-device test states via qa-seeder + media/ # Test media files (images, videos, GIFs) + types/ + DeviceWrapper.ts # Central device abstraction (test interaction goes here) + sessionIt.ts # Test wrapper functions (bothPlatformsIt, androidIt, iosIt) + testing.ts # Shared types (User, StrategyExtractionObj, AccessibilityId) + constants/ # Community config, test file paths + localizer/ # Generated string lookup cache (see External Dependencies) + screenshots/ + android/ # Baseline screenshots for visual regression + ios/ +scripts/ # Device setup scripts +``` + +## Key Abstractions + +### DeviceWrapper (`run/types/DeviceWrapper.ts`) + +The tests interact with devices overwhelmingly through `DeviceWrapper`. It wraps the Appium/WebdriverIO client with higher-level methods (element interaction, scrolling, assertions, screenshot capture, etc.). + +**Platform gating.** `device.onIOS()` and `device.onAndroid()` return a stub that no-ops all calls on the wrong platform. This lets test code call platform-specific APIs without wrapping every call in an `if` block. + +**Self-healing locators.** When a locator fails to find an element, the tests can attempt a fuzzy match against all selectors. If healing succeeds, the test continues and is annotated in the Allure report. The annotation makes it easy to spot brittle locators before they cause hard failures — a healed test is a signal to update the locator, not a silent pass. + +### LocatorsInterface / StrategyExtractionObj (`run/test/locators/index.ts`) + +UI element selectors use one of two forms: + +- **`StrategyExtractionObj` (SEO)** — a plain `{ strategy, selector, text? }` object. Used inline for one-off locators where the selector is the same on both platforms. +- **`LocatorsInterface` (LI)** — an abstract class with a `build(): StrategyExtractionObj` method. Subclass one per UI element when the selector differs by platform or the locator is reused across tests. Platform branching lives inside `build()`, keeping call sites clean. + +Rule of thumb: one-off usage → inline SEO. Platform-specific or reused → LI subclass. + +`DeviceWrapper`'s private `resolveLocator()` method accepts either form transparently, so call sites don't need to know which they're passing. + +### State Builder (`run/test/state_builder/index.ts`) + +Pre-builds complex test states (contacts, group chats) using `@session-foundation/qa-seeder` before the app is opened. This avoids spending the first minutes of every multi-device test manually establishing relationships through the UI. + +Exported functions follow the pattern `open_Alice1_Bob1_friends()`, `open_Alice1_Bob1_Charlie1_friends_group()`, etc. Each returns `{ devices, prebuilt }` where `prebuilt` contains typed `User` objects (`{ userName, accountID, recoveryPhrase }`). + +The `User` type is local. The seeder's `StateUser` type (`sessionId`, `seedPhrase`) is mapped at this boundary and never leaks into test code. + +### Test Wrappers (`run/types/sessionIt.ts`) + +Tests use `bothPlatformsIt()`, `androidIt()`, `iosIt()`, or `bothPlatformsItSeparate()` instead of Playwright's `test()`. Each takes: + +```typescript +{ + title: string; + risk: 'low' | 'medium' | 'high'; + countOfDevicesNeeded: 1 | 2 | 3 | 4; + testCb: (platform, testInfo) => Promise; + shouldSkip?: boolean; + isPro?: boolean; + allureSuites?: { parent: string; suite: string }; + allureDescription?: string; + allureLinks?: { + all?: string[] | string; + android?: string[] | string; + ios?: string[] | string; + }; +} +``` + +The wrapper generates test names with grep tags automatically: `@android`/`@ios`, `@low-risk`/`@medium-risk`/`@high-risk`, `@1-devices` through `@4-devices`, `@pro`. These tags are how CI filters test runs by platform, risk level, or device count. + +## Test Execution Flow + +1. `global-setup.ts` validates that the environment is sane (correct platform env var, reachable network target). +2. Playwright assigns tests to workers. Workers run fully parallel; device allocation is tracked via `run/test/utils/device_registry.ts` to prevent conflicts. +3. The test callback calls a state builder function or `openApp*` directly. Appium connects to the emulator/simulator, installs the app, and restores accounts from recovery phrases. +4. Test steps run against `DeviceWrapper` instances. +5. On failure, `run/test/utils/failure_artifacts.ts` captures screenshots and device logs and attaches them via Playwright's `testInfo.attach()` — these appear in the standard Playwright report regardless of whether Allure is enabled. +6. A `finally` block unregisters devices from the registry regardless of outcome. +7. If `ALLURE_ENABLED=true`, additional Allure metadata (suites, risk, healed locator annotations) is written to the report. + +## External Dependencies + +### `@session-foundation/qa-seeder` + +A package that handles pre-test state: creates users with recovery phrases, provisions groups and communities, and links devices over the network. It is a required dependency for the state builder functions. If the package becomes unavailable or its API drifts out of sync, those functions will fail — but tests that call `openApp*` utilities directly (without the state builder) will continue to work. + +### Visual regression baselines + +Screenshot comparison uses SSIM via `looks-same`. Baselines live in `run/screenshots/{android,ios}/`. On mismatch, diffs are saved to `test-results/diffs/` and attached to the Allure report with a visual comparison UI. + +`UPDATE_BASELINES=true` auto-saves a baseline when none exists. It only runs when the baseline file is missing — it will not overwrite an existing one. To update a baseline after an intentional UI change, delete the old file first, then run with `UPDATE_BASELINES=true`. + +Tests require specific device resolutions (Pixel 6 for Android, iPhone 17 for iOS) to produce consistent screenshots. Using different device models will cause baseline mismatches. + +### Localizer (`run/localizer/`) + +A generated cache of UI strings extracted from the app. Used in tests that assert specific copy. If the app's strings change, the latest strings need to be pulled from the [shared repo](https://github.com/session-foundation/session-localization): + +```bash +git submodule update --init --recursive --remote +``` + +## CI + +The automated regression tests currently run on [self-hosted runners](https://docs.github.com/en/actions/concepts/runners/self-hosted-runners). + +This document assumes that the Node.js/Android/iOS environment outlined in README.md has been set up successfully. + +### Workflows + +| Workflow | Trigger | Purpose | +| ------------------------ | ------------------------------- | ------------------------------------------------------- | +| `android-regression.yml` | Manual dispatch | Full Android test suite on the self-hosted Linux runner | +| `ios-regression.yml` | Manual dispatch | Full iOS test suite on the self-hosted macOS runner | +| `pull.yml` | Pull request | PR validation | +| `deploy-gh-pages.yml` | After Android/iOS completion | Publishes Allure report to GitHub Pages | +| `allure-rollback.yml` | Manual dispatch | Rolls back the last published report | +| `prune-attachments.yml` | Manual dispatch/weekly cron job | Prunes LFS attachment history to keep footprint low | + +### Android + +The Android tests run on a self-hosted Linux machine. The workflow currently identifies this machine by the following runner tags: `[self-hosted, linux, X64, qa-android]` + +4 emulators are booted from a stable (low CPU usage) snapshot. + +The scripts that create and start these devices incl. snapshot management are contained in `scripts/ci.sh`. + +#### Network + +The CI tests are configured to run against a local devnet which is not exposed to the public internet. The self-hosted runner must be on the same network as the devnet to function. + +### iOS + +The iOS tests run on a self-hosted macOS machine. The workflow currently identifies this machine by the following runner tags: `[self-hosted, macOS]` + +12 simulators are booted which are pre-loaded with various media files. + +To set up these devices, run `CI=1 pnpm create-simulators 12` and commit the resulting `ci-simulators.json` to the repository. + +### Allure + +When run on CI, the tests generate and publish test reports to [GitHub Pages](https://session-foundation.github.io/session-appium/) by default. + +The corresponding deployment workflow runs automatically after Android/iOS workflow completion. + +To keep the repository lean, attachments are stored in LFS with their URLs patched to point to GitHub's own CDN. + +The deployment preserves history across runs but it is expected that the `prune-attachments.yml` script is ran periodically to keep the LFS footprint low. + +The last test report can be rolled back on demand with the `allure-rollback.yml` script. + +### Secrets + +To run community admin tests, the 13-word recovery phrase of a Community Admin has been saved under the `SOGS_ADMIN_SEED` secret variable. + +## Maintenance Notes + +**Locators break when app UI changes.** Update the relevant `LocatorsInterface` subclass in `run/test/locators/`. Check the Allure report for self-healed tests first — healing surfaces brittle locators before they become full failures. + +**Baseline screenshots need updating when intentional UI changes ship.** Delete the affected baseline files in `run/screenshots/`, then run the affected tests with `UPDATE_BASELINES=true` against the correct device (Pixel 6 / iPhone 17) to regenerate them, then commit the new images via LFS. + +**iOS simulators need reprovisioning if the macOS runner is rebuilt.** Rerun `CI=1 pnpm create-simulators 12` and commit the new `ci-simulators.json`. + +**Android emulator snapshots** are managed by `scripts/ci.sh`. Refer to that script if the Linux runner needs rebuilding. + +**LFS footprint** grows with each CI run. Run `prune-attachments.yml` periodically. + +**Dependabot** is configured in `.github/dependabot.yml`. diff --git a/README.md b/README.md index e14f8bf8c..619f0a775 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ pnpm test-one 'Test name' # Run specific test (both platforms) pnpm test-one 'Test name @android' # Run specific test on one platform ``` +For CI setup and codebase overview, see [ARCHITECTURE.md](ARCHITECTURE.md). + ## Local Development Note: The tests use devices with specific resolutions for visual regression testing - ensure you have these available (see below). @@ -94,7 +96,7 @@ PLAYWRIGHT_RETRIES_COUNT=0 # Test retry attempts PLAYWRIGHT_REPEAT_COUNT=0 # Successful test repeat count PLAYWRIGHT_WORKERS_COUNT=1 # Parallel test workers CI=0 # Set to 1 to simulate CI (mostly for Allure reporting) -ALLURE_ENABLED='false' # Set to 'true' to generate Allure reports (in conjunction with CI=1) -UPDATE_BASELINES=1 # Auto-save new screenshot baselines if unavailable +ALLURE_ENABLED=false # Set to 'true' to generate Allure reports (in conjunction with CI=1) +UPDATE_BASELINES=true # Auto-save new screenshot baselines if unavailable SOGS_ADMIN_SEED='word1 word2...' # 13-word recovery phrase of an account that's an admin in the testing SOGS. -``` +``` \ No newline at end of file From 2c6b32dbfda7280d6b8c947d430d640dc1742bdc Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:19:21 +1100 Subject: [PATCH 177/184] chore: mention patches in architecture --- ARCHITECTURE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 29eb4ef5a..829357ac3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -176,3 +176,7 @@ To run community admin tests, the 13-word recovery phrase of a Community Admin h **LFS footprint** grows with each CI run. Run `prune-attachments.yml` periodically. **Dependabot** is configured in `.github/dependabot.yml`. + +**pnpm patches** live in `patches/` and are applied automatically on install. If either patched dependency is upgraded, the patch may fail to apply or become redundant — check `pnpm install` output after upgrades: +- `appium-uiautomator2-driver` — expands the device port range to `[8200, 8999]` to support parallel device sessions +- `allure-playwright` — purely cosmetic, renames stdout/stderr labels in the Allure report From 9bc73eac5f04f3d57c08348bc77b56db0f9cb59f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:20:42 +1100 Subject: [PATCH 178/184] chore: lint --- ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 829357ac3..5f059d53b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -178,5 +178,6 @@ To run community admin tests, the 13-word recovery phrase of a Community Admin h **Dependabot** is configured in `.github/dependabot.yml`. **pnpm patches** live in `patches/` and are applied automatically on install. If either patched dependency is upgraded, the patch may fail to apply or become redundant — check `pnpm install` output after upgrades: + - `appium-uiautomator2-driver` — expands the device port range to `[8200, 8999]` to support parallel device sessions - `allure-playwright` — purely cosmetic, renames stdout/stderr labels in the Allure report From e98ac7989d3a7bce0089aa5807835c66fcf747df Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:25:11 +1100 Subject: [PATCH 179/184] fix: remove all force node 24 env vars --- .github/workflows/android-regression.yml | 3 --- .github/workflows/deploy-gh-pages.yml | 8 ++------ .github/workflows/ios-regression.yml | 3 --- github/actions/setup/action.yml | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index e6e07d55a..11d0f98f8 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -72,9 +72,6 @@ jobs: android-regression: runs-on: [self-hosted, linux, X64, qa-android] env: - # TODO: remove once pnpm/action-setup releases a Node 24 version - # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PLATFORM: 'android' APK_URL: ${{ github.event.inputs.APK_URL }} BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index cef4ad099..2759d1fcf 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -22,10 +22,6 @@ concurrency: jobs: deploy: runs-on: ubuntu-latest - env: - # TODO: remove once configure-pages and deploy-pages release Node 24 versions - # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -37,7 +33,7 @@ jobs: submodules: 'recursive' - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Upload artifact uses: actions/upload-pages-artifact@v4 @@ -46,4 +42,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index c732c9a6f..734df4e92 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -73,9 +73,6 @@ jobs: ios-regression: runs-on: [self-hosted, macOS] env: - # TODO: remove once pnpm/action-setup releases a Node 24 version - # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true PLATFORM: 'ios' APK_URL: ${{ github.event.inputs.APK_URL }} BUILD_NUMBER: ${{ github.event.inputs.BUILD_NUMBER }} diff --git a/github/actions/setup/action.yml b/github/actions/setup/action.yml index cfdcef92a..a799df457 100644 --- a/github/actions/setup/action.yml +++ b/github/actions/setup/action.yml @@ -6,7 +6,7 @@ runs: using: 'composite' steps: - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - uses: actions/setup-node@v6 with: From 35488ae03a0badc0dba5abc47c1c8ea6b837e268 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:29:43 +1100 Subject: [PATCH 180/184] chore: bump pnpm on lint --- .github/workflows/pull.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 0db695afe..9cbf79715 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -36,7 +36,7 @@ jobs: key: ${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'patches/**') }} - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v5 - name: Install dependencies shell: bash From 90c3890ca943374130f367117462de321b24514d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 30 Mar 2026 15:56:31 +1100 Subject: [PATCH 181/184] chore: add test context to architecture --- ARCHITECTURE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5f059d53b..c67acb7e9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -54,6 +54,27 @@ Exported functions follow the pattern `open_Alice1_Bob1_friends()`, `open_Alice1 The `User` type is local. The seeder's `StateUser` type (`sessionId`, `seedPhrase`) is mapped at this boundary and never leaks into test code. +### iOS Capabilities & Test Context (`run/test/utils/capabilities_ios.ts`) + +Every iOS session launches with a set of process arguments baked into the shared capabilities: + +- `debugDisappearingMessageDurations: true` — enables shortened disappearing message timers in the app, so DM tests don't have to wait for real-world durations +- `animationsEnabled: false` — disables UI animations for test stability +- `communityPollLimit: 3` — caps community polling frequency + +Per-test overrides are passed via `IOSTestContext`: + +```typescript +type IOSTestContext = { + customInstallTime?: string; // fake first-install timestamp ("time travel") + sessionProEnabled?: string; // enables Session Pro features +}; +``` + +`customInstallTime` injects a `customFirstInstallDateTime` env var into the app process, letting CTA tests simulate the app having been installed days in the past without waiting. `IOSTestContext` threads through the state builder functions and `openApp*` utilities — pass it at the call site to apply it for that test. + +Android handles these behaviours via build flags in the `qa` binary rather than runtime capability overrides, so no equivalent mechanism exists on that side. + ### Test Wrappers (`run/types/sessionIt.ts`) Tests use `bothPlatformsIt()`, `androidIt()`, `iosIt()`, or `bothPlatformsItSeparate()` instead of Playwright's `test()`. Each takes: From b30f0e418b7d4f0c419facda2df520967cc4db53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20M=C3=A1ndoki?= Date: Mon, 30 Mar 2026 16:55:28 +1100 Subject: [PATCH 182/184] Update run/test/utils/create_account.ts Co-authored-by: Audric Ackermann --- run/test/utils/create_account.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/run/test/utils/create_account.ts b/run/test/utils/create_account.ts index 22dbfe563..1f1f1ab6e 100644 --- a/run/test/utils/create_account.ts +++ b/run/test/utils/create_account.ts @@ -55,8 +55,10 @@ export async function newUser( await device.clickOnElementAll(new ContinueButton(device)); // Choose message notification options (Fast mode by default) if (fastMode) { - await device.clickOnElementAll(new FastModeRadio(device)); - } else await device.clickOnElementAll(new SlowModeRadio(device)); +await device.clickOnElementAll(new FastModeRadio(device)); + } else { + await device.clickOnElementAll(new SlowModeRadio(device)); + } await device.clickOnElementAll(new ContinueButton(device)); // Handle permissions based on the flag await handleNotificationPermissions(device, allowNotificationPermissions); From 99f5e5344db9e1756056156afb6f94406aa1c90d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 31 Mar 2026 10:24:27 +1100 Subject: [PATCH 183/184] fix: address PR comments --- run/test/specs/group_tests_create_group_banner.spec.ts | 2 +- run/test/utils/community.ts | 8 ++++---- run/test/utils/create_account.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/run/test/specs/group_tests_create_group_banner.spec.ts b/run/test/specs/group_tests_create_group_banner.spec.ts index f1800bdea..9075a312f 100644 --- a/run/test/specs/group_tests_create_group_banner.spec.ts +++ b/run/test/specs/group_tests_create_group_banner.spec.ts @@ -32,7 +32,7 @@ async function createGroupBanner(platform: SupportedPlatformsType, testInfo: Tes // Open the Create Group screen from home await alice1.clickOnElementAll(new PlusButton(alice1)); await alice1.clickOnElementAll(new CreateGroupOption(alice1)); - // Verify the banner is present + // Verify the banner is not present await alice1.verifyElementNotPresent(new LatestReleaseBanner(alice1)); await closeApp(alice1, bob1); } diff --git a/run/test/utils/community.ts b/run/test/utils/community.ts index 08d10334e..c5a675802 100644 --- a/run/test/utils/community.ts +++ b/run/test/utils/community.ts @@ -29,14 +29,14 @@ export const joinCommunity = async ( await device.scrollToBottom(); }; -export const joinCommunities = async (device: DeviceWrapper, number: number) => { +export const joinCommunities = async (device: DeviceWrapper, toJoin: number) => { const available = Object.values(communities).length; - if (number > available) { + if (toJoin > available) { throw new Error( - `joinCommunities: requested ${number} but only ${available} communities have been recorded.\nCheck run/constants/community.ts for more` + `joinCommunities: requested ${toJoin} but only ${available} communities have been recorded.\nCheck run/constants/community.ts for more` ); } - for (const community of Object.values(communities).slice(0, number)) { + for (const community of Object.values(communities).slice(0, toJoin)) { await joinCommunity(device, community.link, community.name); await device.navigateBack(); } diff --git a/run/test/utils/create_account.ts b/run/test/utils/create_account.ts index 1f1f1ab6e..897a887d8 100644 --- a/run/test/utils/create_account.ts +++ b/run/test/utils/create_account.ts @@ -55,9 +55,9 @@ export async function newUser( await device.clickOnElementAll(new ContinueButton(device)); // Choose message notification options (Fast mode by default) if (fastMode) { -await device.clickOnElementAll(new FastModeRadio(device)); + await device.clickOnElementAll(new FastModeRadio(device)); } else { - await device.clickOnElementAll(new SlowModeRadio(device)); + await device.clickOnElementAll(new SlowModeRadio(device)); } await device.clickOnElementAll(new ContinueButton(device)); // Handle permissions based on the flag From e09e6b693f844940273da742ff37aa339e979590 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Tue, 31 Mar 2026 10:40:50 +1100 Subject: [PATCH 184/184] chore: don't run pro tests by default --- .github/workflows/android-regression.yml | 2 +- .github/workflows/ios-regression.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-regression.yml b/.github/workflows/android-regression.yml index 11d0f98f8..eeddcb227 100644 --- a/.github/workflows/android-regression.yml +++ b/.github/workflows/android-regression.yml @@ -36,7 +36,7 @@ on: RUN_PRO_TESTS: description: 'include Session Pro tests in this run' required: false - default: true + default: false type: boolean ALLURE_ENABLED: diff --git a/.github/workflows/ios-regression.yml b/.github/workflows/ios-regression.yml index 734df4e92..9fd714d90 100644 --- a/.github/workflows/ios-regression.yml +++ b/.github/workflows/ios-regression.yml @@ -27,7 +27,7 @@ on: RUN_PRO_TESTS: description: 'include Session Pro tests in this run' required: false - default: true + default: false type: boolean ALLURE_ENABLED: