From 66398b41ffaf9276e1e3bd746eff1c3dcee194e7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 14:59:22 +1100 Subject: [PATCH 01/11] chore: update strings --- tests/localization/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/localization/lib b/tests/localization/lib index bb08513..c0714a8 160000 --- a/tests/localization/lib +++ b/tests/localization/lib @@ -1 +1 @@ -Subproject commit bb08513f637e7f0fb4de2675b5682a1e129ff4e0 +Subproject commit c0714a8916a38672584323e6084e8cedc36d7243 From c6dd1046256667ed9a83fe3ed482b03abb6f63d2 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:00:09 +1100 Subject: [PATCH 02/11] feat: optionally disable strict mode --- tests/automation/utilities/utils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/automation/utilities/utils.ts b/tests/automation/utilities/utils.ts index 3260bc2..f0015eb 100644 --- a/tests/automation/utilities/utils.ts +++ b/tests/automation/utilities/utils.ts @@ -21,6 +21,7 @@ import { sendMessage } from './message'; type ElementOptions = { maxWait?: number; rightButton?: boolean; + strictMode?: boolean; }; // TODO Unify element interaction functions to use locator objects the way clickOn and clickOnWithText do @@ -278,7 +279,10 @@ export async function clickOn( builtSelector = `css=[${locator.strategy}=${locator.selector}]`; } - const sharedOpts = { timeout: options?.maxWait, strict: true }; + const sharedOpts = { + timeout: options?.maxWait, + strict: options?.strictMode ?? true, + }; await window.click( builtSelector, options?.rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, @@ -311,7 +315,10 @@ export async function clickOnWithText( }]:has-text("${text.replace(/"/g, '\\"')}")`; } - const sharedOpts = { timeout: options?.maxWait, strict: true }; + const sharedOpts = { + timeout: options?.maxWait, + strict: options?.strictMode ?? true, + }; await window.click( builtSelector, options?.rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, From 30b1d9188004998dfb9a3ca3de45644cdef1f96d Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:00:30 +1100 Subject: [PATCH 03/11] feat: wait for failed message tick --- tests/automation/utilities/message.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/automation/utilities/message.ts b/tests/automation/utilities/message.ts index f42dff4..5e7a532 100644 --- a/tests/automation/utilities/message.ts +++ b/tests/automation/utilities/message.ts @@ -33,6 +33,20 @@ export const waitForReadTick = async (window: Page, message: string) => { ); }; +export const waitForFailedTick = async (window: Page, message: string) => { + const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=failed])`; + console.info('waiting for read tick of message: ', message); + + const tickMessageRead = await window.waitForSelector(selc, { + timeout: 30000, + }); + console.info( + 'found the tick of message failed: ', + message, + Boolean(tickMessageRead), + ); +}; + export const sendMessage = async (window: Page, message: string) => { // type into message input box await typeIntoInput(window, 'message-input-text-area', message); From 9ecfbf26deb3aa238de0d64fcbfab2b947493b63 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:01:13 +1100 Subject: [PATCH 04/11] feat: restore account with optional display name --- tests/automation/setup/recovery_using_seed.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/automation/setup/recovery_using_seed.ts b/tests/automation/setup/recovery_using_seed.ts index de3f0a0..a6047a3 100644 --- a/tests/automation/setup/recovery_using_seed.ts +++ b/tests/automation/setup/recovery_using_seed.ts @@ -8,19 +8,31 @@ import { waitForLoadingAnimationToFinish, } from '../utilities/utils'; -export async function recoverFromSeed(window: Page, recoveryPhrase: string) { +export async function recoverFromSeed( + window: Page, + recoveryPhrase: string, + options?: { fallbackName?: string }, +) { await clickOn(window, Onboarding.iHaveAnAccountButton); await typeIntoInput(window, 'recovery-phrase-input', recoveryPhrase); await clickOn(window, Global.continueButton); await waitForLoadingAnimationToFinish(window, 'loading-animation'); - const displayName = await doesElementExist( + const displayNameInput = await doesElementExist( window, 'data-testid', 'display-name-input', ); - if (displayName) { - throw new Error(`Display name was not found when restoring from seed`); + if (displayNameInput) { + if (!options?.fallbackName) { + throw new Error(`Display name was not found when restoring from seed`); + } + // Fallback for when name might be missing (but it's okay) + await typeIntoInput( + window, + Onboarding.displayNameInput.selector, + options.fallbackName, + ); + await clickOn(window, Global.continueButton); } - return { window }; } From 58720c29ef31649fb3e640c4b16784fea92a9abf Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:01:29 +1100 Subject: [PATCH 05/11] feat: add ban/unban tests --- tests/automation/community_tests.spec.ts | 129 ++++++++++++++++++- tests/automation/locators/index.ts | 8 +- tests/automation/types/testing.ts | 4 + tests/automation/utilities/join_community.ts | 46 ++++++- 4 files changed, 180 insertions(+), 7 deletions(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 5c46344..2f9f340 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -1,11 +1,28 @@ import { testCommunityName } from './constants/community'; -import { Conversation, HomeScreen } from './locators'; -import { test_Alice_1W_Bob_1W, test_Alice_2W } from './setup/sessionTest'; -import { joinCommunity } from './utilities/join_community'; -import { sendMessage } from './utilities/message'; +import { Conversation, Global, HomeScreen } from './locators'; +import { newUser } from './setup/new_user'; +import { recoverFromSeed } from './setup/recovery_using_seed'; +import { + sessionTestTwoWindows, + test_Alice_1W_Bob_1W, + test_Alice_2W, +} from './setup/sessionTest'; +import { + assertAdminIsKnown, + joinCommunity, + joinOrOpenCommunity, +} from './utilities/join_community'; +import { sendMessage, waitForFailedTick } from './utilities/message'; import { replyTo } from './utilities/reply_message'; import { sendMedia } from './utilities/send_media'; -import { clickOn, clickOnWithText } from './utilities/utils'; +import { + clickOn, + clickOnWithText, + hasElementBeenDeleted, + hasElementPoppedUpThatShouldnt, + typeIntoInput, + waitForTestIdWithText, +} from './utilities/utils'; test_Alice_2W( 'Join community and sync', @@ -48,3 +65,105 @@ test_Alice_1W_Bob_1W( }); }, ); + +sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { + assertAdminIsKnown(); + const msg1 = `Ban me! - ${Date.now()}`; + const msg2 = `Am I still here? - ${Date.now()}`; + await Promise.all([ + recoverFromSeed(windowA, process.env.SOGS_ADMIN_SEED!, { + fallbackName: 'Admin', + }), + newUser(windowB, 'Bob'), + ]); + await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); + await sendMessage(windowB, msg1); + await clickOnWithText(windowA, Conversation.messageContent, msg1, { + rightButton: true, + }); + await windowA.bringToFront() + await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { + strictMode: false, + }); + await clickOn(windowA, Conversation.banUserButton); + await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); + await clickOn(windowB, Conversation.sendMessageButton); + await waitForFailedTick(windowB, msg2); + await hasElementPoppedUpThatShouldnt( + windowA, + Conversation.messageContent.strategy, + Conversation.messageContent.selector, + msg2, + ); +}); + +sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { + assertAdminIsKnown(); + const msg1 = `Ban me but unban me later! - ${Date.now()}`; + const msg2 = `I'm banned :( - ${Date.now()}`; + const msg3 = `Freedom! - ${Date.now()}` + await Promise.all([ + recoverFromSeed(windowA, process.env.SOGS_ADMIN_SEED!, { + fallbackName: 'Admin', + }), + newUser(windowB, 'Bob'), + ]); + await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); + await sendMessage(windowB, msg1); + await clickOnWithText(windowA, Conversation.messageContent, msg1, { + rightButton: true, + }); + await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { + strictMode: false, + }); + await clickOn(windowA, Conversation.banUserButton); + await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); + await clickOn(windowB, Conversation.sendMessageButton); + await waitForFailedTick(windowB, msg2); + await clickOnWithText(windowA, Conversation.messageContent, msg1, { + rightButton: true, + }); + await clickOnWithText(windowA, Global.contextMenuItem, 'Unban User', { + strictMode: false, + }); + await clickOn(windowA, Conversation.unbanUserButton); + await sendMessage(windowB, msg3); + await waitForTestIdWithText(windowA, Conversation.messageContent.selector, msg3) +}); + +sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { + assertAdminIsKnown(); + const msg1 = `Ban and delete! - ${Date.now()}`; + const msg2 = `Did that work? - ${Date.now()}`; + await Promise.all([ + recoverFromSeed(windowA, process.env.SOGS_ADMIN_SEED!, { + fallbackName: 'Admin', + }), + newUser(windowB, 'Bob'), + ]); + await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); + await sendMessage(windowB, msg1); + await clickOnWithText(windowA, Conversation.messageContent, msg1, { + rightButton: true, + }); + await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { + strictMode: false, + }); + await clickOn(windowA, Conversation.banAndDeleteAllButton); + await hasElementBeenDeleted( + windowA, + Conversation.messageContent.strategy, + Conversation.messageContent.selector, + 10_000, + msg1, + ); + await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); + await clickOn(windowB, Conversation.sendMessageButton); + await waitForFailedTick(windowB, msg2); + await hasElementPoppedUpThatShouldnt( + windowA, + Conversation.messageContent.strategy, + Conversation.messageContent.selector, + msg2, + ); +}); diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index 8079f18..617504a 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -82,6 +82,11 @@ export class Conversation extends Locator { static readonly acceptMessageRequestButton = this.testId( 'accept-message-request', ); + static readonly banAndDeleteAllButton = this.testId( + 'ban-user-delete-all-confirm-button', + ); + static readonly banUserButton = this.testId('ban-user-confirm-button'); + static readonly banUserInput = this.testId('ban-user-input'); static readonly blockMessageRequestButton = this.testId( 'decline-and-block-message-request', ); @@ -101,14 +106,15 @@ export class Conversation extends Locator { static readonly mentionsContainer = this.testId('mentions-container'); // This is also the locator for emojis static readonly mentionsItem = this.testId('mentions-container-row'); // This is also the locator for emojis static readonly messageContent = this.testId('message-content'); - static readonly messageInput = this.testId('message-input-text-area'); + static readonly messageRequestAcceptControlMessage = this.testId( 'message-request-response-message', ); static readonly microphoneButton = this.testId('microphone-button'); static readonly scrollToBottomButton = this.testId('scroll-to-bottom-button'); static readonly sendMessageButton = this.testId('send-message-button'); + static readonly unbanUserButton = this.testId('unban-user-confirm-button') } export class ConversationSettings extends Locator { diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index dcb6367..0716483 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -82,6 +82,9 @@ export type DataTestId = | 'audio-player' | 'avatar-edit-profile-dialog' | 'back-button' + | 'ban-user-confirm-button' + | 'ban-user-delete-all-confirm-button' + | 'ban-user-input' | 'blocked-contacts-settings-row' | 'call-button' | 'call-notification-answered-a-call' @@ -211,6 +214,7 @@ export type DataTestId = | 'swarm-image' | 'theme-section' | 'tooltip-character-count' + | 'unban-user-confirm-button' | 'unblock-button-settings-screen' | 'update-group-info-name-input' | 'update-profile-info-name-input' diff --git a/tests/automation/utilities/join_community.ts b/tests/automation/utilities/join_community.ts index b893900..9a16c7d 100644 --- a/tests/automation/utilities/join_community.ts +++ b/tests/automation/utilities/join_community.ts @@ -1,8 +1,10 @@ -import { Page } from '@playwright/test'; +import { type Page, test } from '@playwright/test'; +import { englishStrippedStr } from '../../localization/englishStrippedStr'; import { type DefaultCommunity, testCommunityLink, + testCommunityName, } from '../constants/community'; import { Global, HomeScreen } from '../locators'; import { @@ -63,3 +65,45 @@ export const leaveCommunity = async (window: Page, communityName: string) => { ); console.log('Left community'); }; + +/** + * There is a race condition where two workers joining the community + * with the same (admin) account can throw the "You are already a member" error + * If joining errors (race condition), attempt to find the on-screen error. + * If the error is visible, the conversation already exists, that's fine, + * just navigate back and open the convo. + */ +export const joinOrOpenCommunity = async (window: Page) => { + try { + await joinCommunity(window); + } catch (joinError) { + try { + await waitForTestIdWithText( + window, + Global.errorMessage.selector, + englishStrippedStr('communityJoinedAlready').toString(), + ); + await clickOn(window, Global.backButton); + await clickOn(window, Global.backButton); + await clickOnWithText( + window, + HomeScreen.conversationItemName, + testCommunityName, + ); + } catch (waitError) { + // The error message we expected wasn't there, so this is a real failure + throw joinError; // Throw the original join error, not the wait timeout + } + } +}; + +export const assertAdminIsKnown = () => { + if (!process.env.SOGS_ADMIN_SEED) { + console.error('SOGS_ADMIN_SEED required.'); + console.error( + 'Promote a user to admin and set their seed as an env variable to run this test.', + ); + console.error('CI runs will use a seed saved as a GitHub secret.'); + test.skip(); + } +}; From 8e4c510dbdcdae789e72dcdaee3724f58636a9c7 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:01:47 +1100 Subject: [PATCH 06/11] chore: linting --- tests/automation/community_tests.spec.ts | 10 +++++++--- tests/automation/locators/index.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 2f9f340..47e6b6e 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -81,7 +81,7 @@ sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); - await windowA.bringToFront() + await windowA.bringToFront(); await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { strictMode: false, }); @@ -101,7 +101,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { assertAdminIsKnown(); const msg1 = `Ban me but unban me later! - ${Date.now()}`; const msg2 = `I'm banned :( - ${Date.now()}`; - const msg3 = `Freedom! - ${Date.now()}` + const msg3 = `Freedom! - ${Date.now()}`; await Promise.all([ recoverFromSeed(windowA, process.env.SOGS_ADMIN_SEED!, { fallbackName: 'Admin', @@ -128,7 +128,11 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { }); await clickOn(windowA, Conversation.unbanUserButton); await sendMessage(windowB, msg3); - await waitForTestIdWithText(windowA, Conversation.messageContent.selector, msg3) + await waitForTestIdWithText( + windowA, + Conversation.messageContent.selector, + msg3, + ); }); sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index 617504a..5f2c80e 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -114,7 +114,7 @@ export class Conversation extends Locator { static readonly microphoneButton = this.testId('microphone-button'); static readonly scrollToBottomButton = this.testId('scroll-to-bottom-button'); static readonly sendMessageButton = this.testId('send-message-button'); - static readonly unbanUserButton = this.testId('unban-user-confirm-button') + static readonly unbanUserButton = this.testId('unban-user-confirm-button'); } export class ConversationSettings extends Locator { From 6185c98767d14769748c0359acea19bbeea53d97 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:06:01 +1100 Subject: [PATCH 07/11] fix: bring windows to front before finding msg --- tests/automation/community_tests.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 47e6b6e..0b409d7 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -78,10 +78,10 @@ sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { ]); await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); + await windowA.bringToFront(); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); - await windowA.bringToFront(); await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { strictMode: false, }); @@ -110,6 +110,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { ]); await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); + await windowA.bringToFront(); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); @@ -147,6 +148,7 @@ sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { ]); await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); + await windowA.bringToFront(); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); From 405eab4ecd4b03dcf89e5f46049a08ca94ce5fb6 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:14:36 +1100 Subject: [PATCH 08/11] fix: try to scroll if possible --- tests/automation/community_tests.spec.ts | 4 ++++ tests/automation/utilities/utils.ts | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 0b409d7..c4d6300 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -20,6 +20,7 @@ import { clickOnWithText, hasElementBeenDeleted, hasElementPoppedUpThatShouldnt, + scrollToBottomIfNecessary, typeIntoInput, waitForTestIdWithText, } from './utilities/utils'; @@ -79,6 +80,7 @@ sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); await windowA.bringToFront(); + await scrollToBottomIfNecessary(windowA); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); @@ -111,6 +113,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); await windowA.bringToFront(); + await scrollToBottomIfNecessary(windowA); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); @@ -149,6 +152,7 @@ sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); await sendMessage(windowB, msg1); await windowA.bringToFront(); + await scrollToBottomIfNecessary(windowA); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); diff --git a/tests/automation/utilities/utils.ts b/tests/automation/utilities/utils.ts index f0015eb..9390dec 100644 --- a/tests/automation/utilities/utils.ts +++ b/tests/automation/utilities/utils.ts @@ -5,7 +5,7 @@ import { ElementHandle, Page } from '@playwright/test'; import { sleepFor } from '../../promise_utils'; -import { CTA } from '../locators'; +import { Conversation, CTA } from '../locators'; import { DataTestId, DMTimeOption, @@ -722,3 +722,14 @@ export async function assertUrlIsReachable(url: string): Promise { ); } } + +export async function scrollToBottomIfNecessary(window: Page): Promise { + const canScroll = await doesElementExist( + window, + Conversation.scrollToBottomButton.strategy, + Conversation.scrollToBottomButton.selector, + ); + if (canScroll) { + await clickOn(window, Conversation.scrollToBottomButton); + } +} From bffcc33195b4eeaf27e7c8481a5b972c06e8ed84 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:31:52 +1100 Subject: [PATCH 09/11] refactor: single message status check util --- tests/automation/community_tests.spec.ts | 8 +-- .../disappearing_message_checks.spec.ts | 4 +- tests/automation/locators/index.ts | 1 - tests/automation/types/testing.ts | 1 + tests/automation/user_actions.spec.ts | 4 +- tests/automation/utilities/message.ts | 56 +++++-------------- tests/automation/utilities/send_media.ts | 6 +- 7 files changed, 26 insertions(+), 54 deletions(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index c4d6300..0594426 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -12,7 +12,7 @@ import { joinCommunity, joinOrOpenCommunity, } from './utilities/join_community'; -import { sendMessage, waitForFailedTick } from './utilities/message'; +import { sendMessage, waitForMessageStatus } from './utilities/message'; import { replyTo } from './utilities/reply_message'; import { sendMedia } from './utilities/send_media'; import { @@ -90,7 +90,7 @@ sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { await clickOn(windowA, Conversation.banUserButton); await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); await clickOn(windowB, Conversation.sendMessageButton); - await waitForFailedTick(windowB, msg2); + await waitForMessageStatus(windowB, msg2, 'failed'); await hasElementPoppedUpThatShouldnt( windowA, Conversation.messageContent.strategy, @@ -123,7 +123,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { await clickOn(windowA, Conversation.banUserButton); await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); await clickOn(windowB, Conversation.sendMessageButton); - await waitForFailedTick(windowB, msg2); + await waitForMessageStatus(windowB, msg2, 'failed'); await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); @@ -169,7 +169,7 @@ sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { ); await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); await clickOn(windowB, Conversation.sendMessageButton); - await waitForFailedTick(windowB, msg2); + await waitForMessageStatus(windowB, msg2, 'failed'); await hasElementPoppedUpThatShouldnt( windowA, Conversation.messageContent.strategy, diff --git a/tests/automation/disappearing_message_checks.spec.ts b/tests/automation/disappearing_message_checks.spec.ts index b1c46f1..4305e12 100644 --- a/tests/automation/disappearing_message_checks.spec.ts +++ b/tests/automation/disappearing_message_checks.spec.ts @@ -16,7 +16,7 @@ import { import { test_Alice_1W_Bob_1W } from './setup/sessionTest'; import { createContact } from './utilities/create_contact'; import { joinCommunity } from './utilities/join_community'; -import { waitForSentTick } from './utilities/message'; +import { waitForMessageStatus } from './utilities/message'; import { sendLinkPreview, sendMedia, @@ -150,7 +150,7 @@ test_Alice_1W_Bob_1W( await typeIntoInput(aliceWindow1, 'message-input-text-area', longText); await sleepFor(100); await clickOn(aliceWindow1, Conversation.sendMessageButton); - await waitForSentTick(aliceWindow1, longText); + await waitForMessageStatus(aliceWindow1, longText, 'sent'); await waitForTextMessage(bobWindow1, longText); // Wait 30 seconds for long text to disappear await sleepFor(30000); diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index 5f2c80e..c9c8b38 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -107,7 +107,6 @@ export class Conversation extends Locator { static readonly mentionsItem = this.testId('mentions-container-row'); // This is also the locator for emojis static readonly messageContent = this.testId('message-content'); static readonly messageInput = this.testId('message-input-text-area'); - static readonly messageRequestAcceptControlMessage = this.testId( 'message-request-response-message', ); diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index 0716483..3d1029e 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -74,6 +74,7 @@ export type WithRightButton = { rightButton?: boolean }; export type MediaType = 'audio' | 'file' | 'image' | 'video'; export type Strategy = ':has-text' | 'class' | 'data-testid'; +export type MessageStatus = 'failed' | 'read' | 'sent'; export type DataTestId = | DMTimeOption diff --git a/tests/automation/user_actions.spec.ts b/tests/automation/user_actions.spec.ts index e11a306..da4af45 100644 --- a/tests/automation/user_actions.spec.ts +++ b/tests/automation/user_actions.spec.ts @@ -17,7 +17,7 @@ import { test_Alice_2W, } from './setup/sessionTest'; import { createContact } from './utilities/create_contact'; -import { sendMessage, waitForReadTick } from './utilities/message'; +import { sendMessage, waitForMessageStatus } from './utilities/message'; import { compareElementScreenshot } from './utilities/screenshot'; import { checkModalStrings, @@ -330,7 +330,7 @@ test_Alice_1W_Bob_1W( HomeScreen.conversationItemName, alice.userName, ); - await waitForReadTick(aliceWindow1, 'Testing read receipts'); + await waitForMessageStatus(aliceWindow1, 'Testing read receipts', 'read'); }, ); diff --git a/tests/automation/utilities/message.ts b/tests/automation/utilities/message.ts index 5e7a532..7bb00fe 100644 --- a/tests/automation/utilities/message.ts +++ b/tests/automation/utilities/message.ts @@ -1,50 +1,22 @@ import { Page } from '@playwright/test'; +import { MessageStatus } from '../types/testing'; // eslint-disable-next-line import/no-cycle import { clickOnElement, typeIntoInput } from './utils'; -export const waitForSentTick = async (window: Page, message: string) => { - // wait for confirmation tick to send reply message - const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=sent])`; - console.info('waiting for sent tick of message: ', message); - - const tickMessageSent = await window.waitForSelector(selc, { - timeout: 30000, - }); - console.info( - 'found the tick of message sent: ', - message, - Boolean(tickMessageSent), - ); -}; - -export const waitForReadTick = async (window: Page, message: string) => { - // wait for confirmation tick to send reply message - const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=read])`; - console.info('waiting for read tick of message: ', message); - - const tickMessageRead = await window.waitForSelector(selc, { - timeout: 30000, - }); - console.info( - 'found the tick of message read: ', - message, - Boolean(tickMessageRead), - ); -}; - -export const waitForFailedTick = async (window: Page, message: string) => { - const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=failed])`; - console.info('waiting for read tick of message: ', message); - - const tickMessageRead = await window.waitForSelector(selc, { - timeout: 30000, +export const waitForMessageStatus = async ( + window: Page, + message: string, + status: MessageStatus, +) => { + const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=${status}])`; + const logSig = `${status} status of message '${message}'`; + console.info(`waiting for ${logSig}`); + + const messageStatus = await window.waitForSelector(selc, { + timeout: 20_000, }); - console.info( - 'found the tick of message failed: ', - message, - Boolean(tickMessageRead), - ); + console.info(`${logSig} is ${Boolean(messageStatus)}`); }; export const sendMessage = async (window: Page, message: string) => { @@ -56,5 +28,5 @@ export const sendMessage = async (window: Page, message: string) => { strategy: 'data-testid', selector: 'send-message-button', }); - await waitForSentTick(window, message); + await waitForMessageStatus(window, message, 'sent'); }; diff --git a/tests/automation/utilities/send_media.ts b/tests/automation/utilities/send_media.ts index 448714e..fad280e 100644 --- a/tests/automation/utilities/send_media.ts +++ b/tests/automation/utilities/send_media.ts @@ -4,7 +4,7 @@ import { englishStrippedStr } from '../../localization/englishStrippedStr'; import { sleepFor } from '../../promise_utils'; import { Conversation, Global, Settings } from '../locators'; import { MediaType } from '../types/testing'; -import { waitForSentTick } from './message'; +import { waitForMessageStatus } from './message'; import { checkModalStrings, clickOn, @@ -81,7 +81,7 @@ export const sendMedia = async ( strategy: 'data-testid', selector: 'send-message-button', }); - await waitForSentTick(window, testMessage); + await waitForMessageStatus(window, testMessage, 'sent'); if (shouldCheckMediaPreview) { await verifyMediaPreviewLoaded(window, testMessage); } @@ -161,7 +161,7 @@ export const sendLinkPreview = async (window: Page, testLink: string) => { selector: 'send-message-button', }); - await waitForSentTick(window, testLink); + await waitForMessageStatus(window, testLink, 'sent'); }; export const trustUser = async ( From 877612e14c6c7306d078791fe91bcb41a7f86d22 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 15:35:39 +1100 Subject: [PATCH 10/11] chore: pass strings check --- tests/automation/enforce_localized_str.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index 39eef6e..14139fc 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -297,6 +297,8 @@ function getExpectedStringFromKey( return 'Plus loads more exclusive features'; case 'remove': return 'Remove'; + case 'communityJoinedAlready': + return 'You are already a member of this community.'; default: // returning null means we don't have an expected string yet for this key. // This will make the test fail From 251da092ae36985774dcce4ee69684fea429e06f Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 5 Feb 2026 16:32:40 +1100 Subject: [PATCH 11/11] chore: use localized strings --- tests/automation/community_tests.spec.ts | 46 ++++--------------- .../automation/enforce_localized_str.spec.ts | 4 ++ 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 0594426..c1adceb 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -1,3 +1,4 @@ +import { englishStrippedStr } from '../localization/englishStrippedStr'; import { testCommunityName } from './constants/community'; import { Conversation, Global, HomeScreen } from './locators'; import { newUser } from './setup/new_user'; @@ -25,6 +26,9 @@ import { waitForTestIdWithText, } from './utilities/utils'; +const banUserString = englishStrippedStr('banUser').toString(); +const unbanUserString = englishStrippedStr('banUnbanUser').toString(); + test_Alice_2W( 'Join community and sync', async ({ aliceWindow1, aliceWindow2 }) => { @@ -67,39 +71,7 @@ test_Alice_1W_Bob_1W( }, ); -sessionTestTwoWindows('Ban User', async ([windowA, windowB]) => { - assertAdminIsKnown(); - const msg1 = `Ban me! - ${Date.now()}`; - const msg2 = `Am I still here? - ${Date.now()}`; - await Promise.all([ - recoverFromSeed(windowA, process.env.SOGS_ADMIN_SEED!, { - fallbackName: 'Admin', - }), - newUser(windowB, 'Bob'), - ]); - await Promise.all([joinOrOpenCommunity(windowA), joinCommunity(windowB)]); - await sendMessage(windowB, msg1); - await windowA.bringToFront(); - await scrollToBottomIfNecessary(windowA); - await clickOnWithText(windowA, Conversation.messageContent, msg1, { - rightButton: true, - }); - await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { - strictMode: false, - }); - await clickOn(windowA, Conversation.banUserButton); - await typeIntoInput(windowB, Conversation.messageInput.selector, msg2); - await clickOn(windowB, Conversation.sendMessageButton); - await waitForMessageStatus(windowB, msg2, 'failed'); - await hasElementPoppedUpThatShouldnt( - windowA, - Conversation.messageContent.strategy, - Conversation.messageContent.selector, - msg2, - ); -}); - -sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { +sessionTestTwoWindows('Ban and unban user', async ([windowA, windowB]) => { assertAdminIsKnown(); const msg1 = `Ban me but unban me later! - ${Date.now()}`; const msg2 = `I'm banned :( - ${Date.now()}`; @@ -117,7 +89,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); - await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { + await clickOnWithText(windowA, Global.contextMenuItem, banUserString, { strictMode: false, }); await clickOn(windowA, Conversation.banUserButton); @@ -127,7 +99,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); - await clickOnWithText(windowA, Global.contextMenuItem, 'Unban User', { + await clickOnWithText(windowA, Global.contextMenuItem, unbanUserString, { strictMode: false, }); await clickOn(windowA, Conversation.unbanUserButton); @@ -139,7 +111,7 @@ sessionTestTwoWindows('Unban User', async ([windowA, windowB]) => { ); }); -sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { +sessionTestTwoWindows('Ban And delete all', async ([windowA, windowB]) => { assertAdminIsKnown(); const msg1 = `Ban and delete! - ${Date.now()}`; const msg2 = `Did that work? - ${Date.now()}`; @@ -156,7 +128,7 @@ sessionTestTwoWindows('Ban And Delete All', async ([windowA, windowB]) => { await clickOnWithText(windowA, Conversation.messageContent, msg1, { rightButton: true, }); - await clickOnWithText(windowA, Global.contextMenuItem, 'Ban User', { + await clickOnWithText(windowA, Global.contextMenuItem, banUserString, { strictMode: false, }); await clickOn(windowA, Conversation.banAndDeleteAllButton); diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index 14139fc..9e58390 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -299,6 +299,10 @@ function getExpectedStringFromKey( return 'Remove'; case 'communityJoinedAlready': return 'You are already a member of this community.'; + case 'banUser': + return 'Ban User'; + case 'banUnbanUser': + return 'Unban User'; default: // returning null means we don't have an expected string yet for this key. // This will make the test fail