Skip to content
111 changes: 106 additions & 5 deletions tests/automation/community_tests.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import { englishStrippedStr } from '../localization/englishStrippedStr';
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, waitForMessageStatus } 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,
scrollToBottomIfNecessary,
typeIntoInput,
waitForTestIdWithText,
} from './utilities/utils';

const banUserString = englishStrippedStr('banUser').toString();
const unbanUserString = englishStrippedStr('banUnbanUser').toString();

test_Alice_2W(
'Join community and sync',
Expand Down Expand Up @@ -48,3 +70,82 @@ test_Alice_1W_Bob_1W(
});
},
);

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()}`;
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 windowA.bringToFront();
await scrollToBottomIfNecessary(windowA);
await clickOnWithText(windowA, Conversation.messageContent, msg1, {
rightButton: true,
});
await clickOnWithText(windowA, Global.contextMenuItem, banUserString, {
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 clickOnWithText(windowA, Conversation.messageContent, msg1, {
rightButton: true,
});
await clickOnWithText(windowA, Global.contextMenuItem, unbanUserString, {
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 windowA.bringToFront();
await scrollToBottomIfNecessary(windowA);
await clickOnWithText(windowA, Conversation.messageContent, msg1, {
rightButton: true,
});
await clickOnWithText(windowA, Global.contextMenuItem, banUserString, {
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 waitForMessageStatus(windowB, msg2, 'failed');
await hasElementPoppedUpThatShouldnt(
windowA,
Conversation.messageContent.strategy,
Conversation.messageContent.selector,
msg2,
);
});
4 changes: 2 additions & 2 deletions tests/automation/disappearing_message_checks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions tests/automation/enforce_localized_str.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ function getExpectedStringFromKey(
return 'Plus loads more exclusive features';
case 'remove':
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
Expand Down
7 changes: 6 additions & 1 deletion tests/automation/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand All @@ -101,14 +106,14 @@ 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 {
Expand Down
22 changes: 17 additions & 5 deletions tests/automation/setup/recovery_using_seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
5 changes: 5 additions & 0 deletions tests/automation/types/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -82,6 +83,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'
Expand Down Expand Up @@ -211,6 +215,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'
Expand Down
4 changes: 2 additions & 2 deletions tests/automation/user_actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
},
);

Expand Down
46 changes: 45 additions & 1 deletion tests/automation/utilities/join_community.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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();
}
};
40 changes: 13 additions & 27 deletions tests/automation/utilities/message.ts
Original file line number Diff line number Diff line change
@@ -1,36 +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);
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 tickMessageSent = await window.waitForSelector(selc, {
timeout: 30000,
const messageStatus = await window.waitForSelector(selc, {
timeout: 20_000,
});
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),
);
console.info(`${logSig} is ${Boolean(messageStatus)}`);
};

export const sendMessage = async (window: Page, message: string) => {
Expand All @@ -42,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');
};
Loading