iOS & Android: Migrate 29 Channel Settings tests from Rainforest to Detox (#9313)

This commit is contained in:
yasser khan 2026-02-09 19:46:27 +05:30 committed by GitHub
parent 121980c7e5
commit 95d50125e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2088 additions and 1112 deletions

View file

@ -115,7 +115,7 @@ jobs:
id: generate-specs
uses: ./.github/actions/generate-specs
with:
parallelism: 5
parallelism: 10
search_path: detox/e2e/test
device_name: ${{ env.DEVICE_NAME }}
device_os_version: ${{ env.DEVICE_OS_VERSION }}
@ -244,8 +244,8 @@ jobs:
# Open Simulator.app in background to speed up UI rendering
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID"
# Give Simulator.app time to fully initialize UI
sleep 3
# Give Simulator.app time to fully initialize UI (iOS 26.2 needs more time)
sleep 5
echo "SIMULATOR_ID=$SIMULATOR_ID" >> $GITHUB_ENV
@ -319,9 +319,9 @@ jobs:
tail -30 metro.log
else
echo "Metro is serving on http://localhost:8081"
# Give Metro extra time to fully initialize for iOS 26.2
# Give Metro extra time to fully initialize for iOS 26.2 (React Native bridge needs more time)
echo "Allowing Metro to fully stabilize..."
sleep 3
sleep 8
fi
cd detox

View file

@ -83,10 +83,10 @@
"behavior": {
"init": {
"reinstallApp": false,
"launchApp": true
"launchApp": false
},
"cleanup": {
"shutdownDevice": true
"shutdownDevice": false
}
},
"session": {

View file

@ -181,4 +181,3 @@ To run Playbooks tests:
For complete documentation, see:
- [Wix Pilot Technical Overview](https://wix-pilot.com/docs/guides/technical-overview)
- [Pilot Best Practices Guide](https://wix-pilot.com/docs/guides/pilot-best-practices)

View file

@ -6,7 +6,7 @@ const shard = process.env.CI_NODE_INDEX ? process.env.CI_NODE_INDEX : '';
module.exports = {
setupFilesAfterEnv: ['./test/setup.ts'],
maxWorkers: 1,
maxWorkers: process.env.CI ? 1 : 2,
testSequencer: './custom_sequencer.js',
testTimeout: process.env.LOW_BANDWIDTH_MODE === 'true' ? 240000 : 180000,
rootDir: '.',

View file

@ -1,80 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
import {execSync} from 'child_process';
/**
* Check if we're running on Android platform
* Uses environment variable as fallback if device is not initialized
*/
function isAndroid(): boolean {
try {
// Try to get platform from device (if initialized)
if (typeof device !== 'undefined' && device.getPlatform) {
return device.getPlatform() === 'android';
}
} catch {
// Device not initialized yet, fall back to env var
}
// Fall back to environment variable check
return process.env.IOS !== 'true';
}
/**
* Clean up ADB reverse port mappings to prevent "Address already in use" errors
* This is particularly important in CI environments where previous test runs may have crashed
* iOS: This function safely no-ops on iOS - no side effects
*/
export async function cleanupAdbReversePorts(): Promise<void> {
// Only run on Android - safe no-op for iOS
if (!isAndroid()) {
return;
}
try {
console.info('[ADB Cleanup] Removing all reverse port mappings...');
// Remove all reverse port mappings
execSync('adb reverse --remove-all', {
stdio: 'pipe',
timeout: 5000,
});
console.info('[ADB Cleanup] Successfully cleaned up reverse port mappings');
} catch (error) {
// Don't fail if cleanup fails - this is best-effort
console.warn(`[ADB Cleanup] Warning: Failed to cleanup reverse ports: ${(error as Error).message}`);
}
}
/**
* Reset ADB server connection to clear any stale state
* This can help recover from connection issues in CI
* iOS: This function safely no-ops on iOS - no side effects
*/
export async function resetAdbServer(): Promise<void> {
// Only run on Android - safe no-op for iOS
if (!isAndroid()) {
return;
}
try {
console.info('[ADB Reset] Restarting ADB server...');
// Kill and restart ADB server
execSync('adb kill-server && adb start-server', {
stdio: 'pipe',
timeout: 10000,
});
// Wait a bit for server to stabilize
await new Promise((resolve) => setTimeout(resolve, 2000));
console.info('[ADB Reset] Successfully restarted ADB server');
} catch (error) {
// Don't fail if reset fails - this is best-effort
console.warn(`[ADB Reset] Warning: Failed to reset ADB server: ${(error as Error).message}`);
}
}

View file

@ -40,6 +40,7 @@ class Alert {
// alert buttons
cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(1);
confirmButton = isAndroid() ? element(by.text('CONFIRM')) : element(by.label('Confirm')).atIndex(1);
doneButton = isAndroid() ? element(by.text('DONE')) : element(by.label('Done')).atIndex(1);
deleteButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(0);
deleteScheduledMessageButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(1);
leaveButton = isAndroid() ? element(by.text('LEAVE')) : element(by.label('Leave')).atIndex(0);

View file

@ -0,0 +1,77 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ProfilePicture} from '@support/ui/component';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
class AddMembersScreen {
testID = {
addMembersScreen: 'add_members.screen',
searchInput: 'add_members.search_bar.search.input',
userList: 'add_members.user_list',
userItemPrefix: 'add_members.user_list.user_item.',
addButton: 'add_members.add.button',
addChannelMembersButton: 'add_members.selected.start.button',
backButton: 'screen.back.button',
tutorialTooltip: 'tutorial_highlight',
};
addChannelMembersButton = element(by.id(this.testID.addChannelMembersButton));
addMembersScreen = element(by.id(this.testID.addMembersScreen));
searchInput = element(by.id(this.testID.searchInput));
userList = element(by.id(this.testID.userList));
addButton = element(by.id(this.testID.addButton));
backButton = element(by.id(this.testID.backButton));
tutorialTooltip = element(by.id(this.testID.tutorialTooltip));
getUserItem = (userId: string) => {
return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}`));
};
getUserItemProfilePicture = (userId: string) => {
return element(ProfilePicture.getProfilePictureItemMatcher(this.testID.userItemPrefix, userId));
};
getUserItemDisplayName = (userId: string) => {
return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}.display_name`));
};
toBeVisible = async () => {
await waitFor(this.searchInput).toBeVisible().withTimeout(timeouts.TEN_SEC);
return this.addMembersScreen;
};
close = async () => {
await this.backButton.tap();
await expect(this.addMembersScreen).not.toBeVisible();
};
dismissTutorial = async () => {
try {
const tutorialText = element(by.text('Long-press on an item to view a user\'s profile'));
await waitFor(tutorialText).toBeVisible().withTimeout(timeouts.TWO_SEC);
await tutorialText.tap();
await wait(timeouts.HALF_SEC);
} catch {
// Tutorial not visible, continue
}
};
searchAndAddUser = async (username: string, userId: string) => {
await this.searchInput.typeText(`${username}\n`);
await wait(timeouts.TWO_SEC);
const userItem = this.getUserItem(userId);
await waitFor(userItem).toBeVisible().withTimeout(timeouts.TEN_SEC);
await userItem.tap();
await wait(timeouts.ONE_SEC);
await waitFor(this.addChannelMembersButton).toBeVisible().withTimeout(timeouts.TEN_SEC);
await this.addChannelMembersButton.tap();
await wait(timeouts.TWO_SEC);
};
}
const addMembersScreen = new AddMembersScreen();
export default addMembersScreen;

View file

@ -17,7 +17,7 @@ import {
PostOptionsScreen,
ThreadScreen,
} from '@support/ui/screen';
import {isAndroid, isIos, timeouts, wait} from '@support/utils';
import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
class ChannelScreen {
@ -153,13 +153,13 @@ class ChannelScreen {
dismissScheduledPostTooltip = async () => {
// Try to close scheduled post tooltip if it exists (try both regular and admin account versions)
try {
await waitFor(this.scheduledPostTooltipCloseButton).toBeVisible().withTimeout(timeouts.ONE_SEC);
await waitFor(this.scheduledPostTooltipCloseButton).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await this.scheduledPostTooltipCloseButton.tap();
await wait(timeouts.HALF_SEC);
} catch {
// Try admin account version
try {
await waitFor(this.scheduledPostTooltipCloseButtonAdminAccount).toBeVisible().withTimeout(timeouts.ONE_SEC);
await waitFor(this.scheduledPostTooltipCloseButtonAdminAccount).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await this.scheduledPostTooltipCloseButtonAdminAccount.tap();
await wait(timeouts.HALF_SEC);
} catch {
@ -204,23 +204,6 @@ class ChannelScreen {
}
};
dismissKeyboard = async () => {
// Explicitly dismiss keyboard before long press
if (isAndroid()) {
try {
await device.pressBack();
await wait(timeouts.THREE_SEC);
} catch (error) {
// Keyboard might not be open, continue
}
}
if (isIos()) {
// On iOS, tap outside the input area to dismiss keyboard
await this.postInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
}
};
openPostOptionsFor = async (postId: string, text: string) => {
const {postListPostItem} = this.getPostListPostItem(postId, text);
await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.TEN_SEC);
@ -243,7 +226,7 @@ class ChannelScreen {
// # Post message
await this.postInput.tap();
await this.postInput.clearText();
await this.postInput.typeText(message);
await this.postInput.replaceText(`${message}\n`);
await this.tapSendButton();
await wait(timeouts.TWO_SEC);
};

View file

@ -233,6 +233,58 @@ class ChannelInfoScreen {
unarchivePublicChannel = async ({confirm = true} = {}) => {
await this.unarchiveChannel(Alert.unarchivePublicChannelTitle, {confirm});
};
copyChannelHeader = async (headerText: string) => {
// Long press on header text
await element(by.text(headerText)).longPress();
// Wait for bottom sheet
await waitFor(element(by.id('channel_info.extra.header.bottom_sheet.copy_header_text'))).
toBeVisible().
withTimeout(timeouts.TWO_SEC);
// Tap copy option (actual copy action)
await element(by.id('channel_info.extra.header.bottom_sheet.copy_header_text')).tap();
};
cancelCopyChannelHeader = async (headerText: string) => {
// Long press on header text
await element(by.text(headerText)).longPress();
// Wait for bottom sheet
await waitFor(element(by.id('channel_info.extra.header.bottom_sheet.copy_header_text'))).
toBeVisible().
withTimeout(timeouts.TWO_SEC);
// Cancel
await element(by.id('channel_info.extra.header.bottom_sheet.cancel')).tap();
};
copyChannelPurpose = async (purposeText: string) => {
// Long press on purpose text
await element(by.text(purposeText)).longPress();
// Wait for bottom sheet
await waitFor(element(by.id('channel_info.title.public_private.bottom_sheet.copy_purpose'))).
toBeVisible().
withTimeout(timeouts.TWO_SEC);
// Tap copy option
await element(by.id('channel_info.title.public_private.bottom_sheet.copy_purpose')).tap();
};
cancelCopyChannelPurpose = async (purposeText: string) => {
// Long press on purpose text
await element(by.text(purposeText)).longPress();
// Wait for bottom sheet
await waitFor(element(by.id('channel_info.title.public_private.bottom_sheet.copy_purpose'))).
toBeVisible().
withTimeout(timeouts.TWO_SEC);
// Cancel
await element(by.id('channel_info.title.public_private.bottom_sheet.cancel')).tap();
};
}
const channelInfoScreen = new ChannelInfoScreen();

View file

@ -77,7 +77,7 @@ class CreateDirectMessageScreen {
await ChannelListScreen.headerPlusButton.tap();
await wait(timeouts.ONE_SEC);
await ChannelListScreen.openDirectMessageItem.tap();
await wait(timeouts.TEN_SEC);
await wait(timeouts.FOUR_SEC);
return this.toBeVisible();
};

View file

@ -3,6 +3,7 @@
import AboutScreen from './about';
import AccountScreen from './account';
import AddMembersScreen from './add_members';
import AdvancedSettingsScreen from './advanced_settings';
import AutoResponderNotificationSettingsScreen from './auto_responder_notification_settings';
import BrowseChannelsScreen from './browse_channels';
@ -55,6 +56,7 @@ import UserProfileScreen from './user_profile';
export {
AboutScreen,
AccountScreen,
AddMembersScreen,
AdvancedSettingsScreen,
AutoResponderNotificationSettingsScreen,
BrowseChannelsScreen,

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {serverOneUrl} from '@support/test_config';
import {ChannelListScreen, ServerScreen} from '@support/ui/screen';
import {isAndroid, retryWithReload, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
@ -79,7 +80,15 @@ class LoginScreen {
while (attempt < maxAttempts) {
try {
// eslint-disable-next-line no-await-in-loop
await retryWithReload(() => this.loginWithRetryIfStuck(user));
await retryWithReload(
async () => {
return this.loginWithRetryIfStuck(user);
},
3, // retries
ServerScreen,
serverOneUrl, // serverUrl - reconnect after reload
'Server 1', // serverDisplayName
);
return;
} catch (error) {
lastError = error;

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ProfilePicture} from '@support/ui/component';
import {Alert, ProfilePicture} from '@support/ui/component';
import {ChannelInfoScreen} from '@support/ui/screen';
import {isIos, timeouts, wait} from '@support/utils';
import {expect, waitFor} from 'detox';
@ -10,20 +10,28 @@ class ManageChannelMembersScreen {
testID = {
backButton: 'screen.back.button',
manageMembersScreen: 'manage_members.screen',
channelMembersScreen: 'channel_members.screen',
manageDoneButton: 'manage_members.button', // Same button, text changes between "Manage" and "Done"
searchBar: 'manage_members.search_bar',
searchInput: 'manage_members.search_bar.search.input',
userList: 'manage_members.user_list',
userItemPrefix: 'create_direct_message.user_list.user_item.',
userItemPrefix: 'manage_members.user_list.user_item.',
removeButton: 'channel.remove_member',
notice: 'manage_members.notice',
tutorialHighlight: 'tutorial_highlight',
tutorialSwipeLeft: 'tutorial_swipe_left',
gmMemberSectionList: 'manage_members.user_list.section_list',
};
gmMemberSectionList = element(by.id(this.testID.gmMemberSectionList));
manageMembersScreen = element(by.id(this.testID.manageMembersScreen));
channelMembersScreen = element(by.id(this.testID.channelMembersScreen));
manageButton = element(by.id(this.testID.manageDoneButton));
doneButton = element(by.id(this.testID.manageDoneButton)); // Same element as manageButton, different text
searchBar = element(by.id(this.testID.searchBar));
searchInput = element(by.id(this.testID.searchInput));
userList = element(by.id(this.testID.userList));
removeButton = element(by.id(this.testID.removeButton));
notice = element(by.id(this.testID.notice));
tutorialHighlight = element(by.id(this.testID.tutorialHighlight));
tutorialSwipeLeft = element(by.id(this.testID.tutorialSwipeLeft));
@ -91,6 +99,26 @@ class ManageChannelMembersScreen {
console.log('Tutorial element not visible, skipping action:');
}
};
searchAndRemoveUser = async (username: string, userId: string) => {
await expect(this.searchInput).toBeVisible();
await this.searchInput.typeText(`${username}`);
await wait(timeouts.TWO_SEC);
const userItem = this.getUserItem(userId);
await waitFor(userItem).toBeVisible().withTimeout(timeouts.TEN_SEC);
await userItem.tap();
await wait(timeouts.TWO_SEC);
await expect(this.removeButton).toBeVisible();
await this.removeButton.tap();
await wait(timeouts.TWO_SEC);
await Alert.removeButton.tap();
await wait(timeouts.TWO_SEC);
await device.pressBack();
};
}
const manageChannelMembersScreen = new ManageChannelMembersScreen();

View file

@ -75,7 +75,7 @@ class ServerScreen {
// The bridge can be busy during login transition, use waitFor without idle check
const timeout = isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN;
await waitFor(this.usernameInput).toBeVisible().withTimeout(timeout);
await waitForElementToBeVisible(this.usernameInput, timeout, timeouts.ONE_SEC);
};
close = async () => {

View file

@ -118,7 +118,7 @@ class ThreadScreen {
postMessage = async (message: string) => {
// # Post message
await this.postInput.tap();
await this.postInput.replaceText(message);
await this.postInput.replaceText(`${message}\n`);
await this.tapSendButton();
// # Wait for message to be rendered

View file

@ -24,58 +24,6 @@ export async function waitForLoadingSpinner(testID: string, timeout = 10000): Pr
await waitFor(element(by.id(testID))).not.toBeVisible().withTimeout(timeout);
}
/**
* Safely dismiss keyboard with retry logic
* Prevents keyboard input session invalidation errors
* @param maxAttempts - Maximum number of dismissal attempts
*/
export async function dismissKeyboardSafely(maxAttempts = 3): Promise<void> {
/* eslint-disable no-await-in-loop, no-console */
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await device.pressBack(); // Works on both iOS (dismisses keyboard) and Android
// Wait a bit for keyboard to dismiss
await new Promise((resolve) => setTimeout(resolve, 300));
return;
} catch (error) {
if (attempt === maxAttempts) {
// On final attempt, log warning but don't throw to prevent test failures
console.warn(`[dismissKeyboardSafely] Failed to dismiss keyboard after ${maxAttempts} attempts:`, error);
} else {
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
}
/* eslint-enable no-await-in-loop, no-console */
}
/**
* Perform text input with keyboard state verification
* Ensures text input session is valid before typing
* @param element - Detox element to type into
* @param text - Text to type
*/
export async function typeTextSafely(elementToType: Detox.IndexableNativeElement, text: string): Promise<void> {
try {
// Tap element to ensure it's focused
await elementToType.tap();
await new Promise((resolve) => setTimeout(resolve, 300));
// Type text
await elementToType.typeText(text);
} catch (error) {
// eslint-disable-next-line no-console
console.warn('[typeTextSafely] Text input failed, retrying:', error);
// Retry once with dismissal and refocus
await dismissKeyboardSafely();
await new Promise((resolve) => setTimeout(resolve, 500));
await elementToType.tap();
await new Promise((resolve) => setTimeout(resolve, 300));
await elementToType.typeText(text);
}
}
/**
* Retry an element visibility check with exponential backoff
* Helps handle race conditions during navigation and UI transitions

View file

@ -83,10 +83,18 @@ export const timeouts = {
* Retry a function with reload
* @param {function} func - function to retry
* @param {number} retries - number of retries
* @param {string} serverUrl - optional server URL to reconnect after reload
* @param {string} serverDisplayName - optional server display name to reconnect after reload
* @return {Promise<void>} - promise that resolves when the function succeeds
* @throws {Error} - if the function fails after the specified number of retries
*/
export async function retryWithReload(func: () => Promise<void>, retries: number = 2): Promise<void> {
export async function retryWithReload(
func: () => Promise<void>,
retries: number = 2,
ServerScreen: any,
serverUrl?: string,
serverDisplayName?: string,
): Promise<void> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
// eslint-disable-next-line no-await-in-loop
@ -97,7 +105,14 @@ export async function retryWithReload(func: () => Promise<void>, retries: number
// eslint-disable-next-line no-await-in-loop
await device.reloadReactNative();
// eslint-disable-next-line no-await-in-loop
await new Promise((res) => setTimeout(res, 3000));
await new Promise((res) => setTimeout(res, 10000));
// If server connection details provided, reconnect after reload
if (serverUrl && serverDisplayName) {
// Dynamically import to avoid circular dependencies
// eslint-disable-next-line no-await-in-loop
await ServerScreen.connectToServer(serverUrl, serverDisplayName);
}
} else {
throw err;
}

View file

@ -394,7 +394,6 @@ describe('Account - Custom Status', () => {
await ChannelListScreen.open();
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(messageText);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem, postListPostItemHeaderDisplayName} =

View file

@ -45,7 +45,6 @@ describe('Autocomplete - Edit Post', () => {
const message = `Messsage ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, channel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, channel.id);
await ChannelScreen.openPostOptionsFor(post.id, message);
await EditPostScreen.open();

View file

@ -0,0 +1,276 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
/**
* Test Cases Included:
* - MM-T3201: RN apps Create public channel
* - MM-T3203: RN apps Create private channel
* - MM-T3199: RN apps Edit public channel
* - MM-T3206: RN apps Edit private channel
* - MM-T854: RN apps Channel can be created using 2 non-latin characters
* - MM-T867: RN apps Copying channel header text
* - MM-T865: RN apps Copying channel purpose text
*/
import {Channel, Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelInfoScreen,
ChannelListScreen,
ChannelScreen,
CreateOrEditChannelScreen,
LoginScreen,
HomeScreen,
ServerScreen,
} from '@support/ui/screen';
import {getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testUser: any;
let testTeam: any;
let publicChannelName: string;
let publicChannelDisplayName: string;
let privateChannelName: string;
let privateChannelDisplayName: string;
let channelWithMetadata: any;
beforeAll(async () => {
const {user, team} = await Setup.apiInit(siteOneUrl);
testUser = user;
testTeam = team;
// Create a channel with header and purpose for copy tests
const {channel: metadataChannel} = await Channel.apiCreateChannel(siteOneUrl, {
teamId: testTeam.id,
name: `channel-metadata-${getRandomId()}`,
displayName: `Channel Metadata ${getRandomId()}`,
type: 'O',
header: 'This is test header',
purpose: 'Test purpose for copying',
});
channelWithMetadata = metadataChannel;
await wait(timeouts.THREE_SEC);
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, channelWithMetadata.id);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
await HomeScreen.logout();
});
it('MM-T3201 - RN apps Create public channel', async () => {
publicChannelName = `channel-${getRandomId()}`;
publicChannelDisplayName = publicChannelName.replace(/-/g, ' ');
const channelPurpose = 'This is a test purpose for the channel';
const channelHeader = ':taco:';
await ChannelListScreen.headerPlusButton.tap();
await ChannelListScreen.createNewChannelItem.tap();
await CreateOrEditChannelScreen.toBeVisible();
await expect(CreateOrEditChannelScreen.displayNameInput).toBeVisible();
await expect(CreateOrEditChannelScreen.createButton).toBeVisible();
await CreateOrEditChannelScreen.displayNameInput.replaceText(publicChannelDisplayName);
await CreateOrEditChannelScreen.purposeInput.tap();
await CreateOrEditChannelScreen.purposeInput.replaceText(channelPurpose);
await CreateOrEditChannelScreen.headerInput.tap();
await CreateOrEditChannelScreen.headerInput.replaceText(channelHeader);
await CreateOrEditChannelScreen.createButton.tap();
await ChannelScreen.dismissScheduledPostTooltip();
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(publicChannelDisplayName);
await ChannelInfoScreen.open();
await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(publicChannelDisplayName);
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(channelPurpose);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T3203 - RN apps Create private channel', async () => {
privateChannelName = `private-channel-${getRandomId()}`;
privateChannelDisplayName = privateChannelName.replace(/-/g, ' ');
const channelPurpose = 'This is a private test channel purpose';
const channelHeader = 'Private channel header';
await ChannelListScreen.headerPlusButton.tap();
await ChannelListScreen.createNewChannelItem.tap();
await CreateOrEditChannelScreen.toBeVisible();
await expect(CreateOrEditChannelScreen.displayNameInput).toBeVisible();
await CreateOrEditChannelScreen.toggleMakePrivateOn();
await expect(CreateOrEditChannelScreen.makePrivateToggledOn).toBeVisible();
await CreateOrEditChannelScreen.displayNameInput.replaceText(privateChannelDisplayName);
await CreateOrEditChannelScreen.purposeInput.tap();
await CreateOrEditChannelScreen.purposeInput.replaceText(channelPurpose);
await CreateOrEditChannelScreen.headerInput.tap();
await CreateOrEditChannelScreen.headerInput.replaceText(channelHeader);
await CreateOrEditChannelScreen.createButton.tap();
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(privateChannelDisplayName);
await ChannelInfoScreen.open();
await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(privateChannelDisplayName);
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(channelPurpose);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T3199 - RN apps Edit public channel', async () => {
await ChannelScreen.open(channelsCategory, publicChannelName);
await ChannelInfoScreen.open();
await CreateOrEditChannelScreen.openEditChannel();
await expect(CreateOrEditChannelScreen.saveButton).toBeVisible();
const updatedDisplayName = publicChannelDisplayName + ' edited';
await CreateOrEditChannelScreen.displayNameInput.clearText();
await CreateOrEditChannelScreen.displayNameInput.replaceText(updatedDisplayName);
const purposeText = 'Updated purpose for this channel';
await CreateOrEditChannelScreen.purposeInput.tap();
await CreateOrEditChannelScreen.purposeInput.clearText();
await CreateOrEditChannelScreen.purposeInput.replaceText(purposeText);
const headerLine1 = 'First line of header';
await CreateOrEditChannelScreen.headerInput.tap();
await CreateOrEditChannelScreen.headerInput.clearText();
await CreateOrEditChannelScreen.headerInput.replaceText(headerLine1 + '\n');
const headerLine2 = 'Second line of header';
await CreateOrEditChannelScreen.headerInput.replaceText(headerLine2);
await CreateOrEditChannelScreen.saveButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelInfoScreen.toBeVisible();
await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(updatedDisplayName);
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(purposeText);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T3206 - RN apps Edit private channel', async () => {
await ChannelScreen.open(channelsCategory, privateChannelName);
await ChannelInfoScreen.open();
await CreateOrEditChannelScreen.openEditChannel();
await expect(CreateOrEditChannelScreen.saveButton).toBeVisible();
const updatedDisplayName = privateChannelDisplayName + ' edited';
await CreateOrEditChannelScreen.displayNameInput.clearText();
await CreateOrEditChannelScreen.displayNameInput.replaceText(updatedDisplayName);
const purposeText = 'Updated purpose for private channel';
await CreateOrEditChannelScreen.purposeInput.tap();
await CreateOrEditChannelScreen.purposeInput.clearText();
await CreateOrEditChannelScreen.purposeInput.replaceText(purposeText);
const headerLine1 = 'First line of header';
await CreateOrEditChannelScreen.headerInput.tap();
await CreateOrEditChannelScreen.headerInput.clearText();
await CreateOrEditChannelScreen.headerInput.replaceText(headerLine1 + '\n');
const headerLine2 = 'Second line of header';
await CreateOrEditChannelScreen.headerInput.replaceText(headerLine2);
await CreateOrEditChannelScreen.saveButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelInfoScreen.toBeVisible();
await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(updatedDisplayName);
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(purposeText);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T854 - RN apps Channel can be created using 2 non-latin characters', async () => {
await ChannelListScreen.headerPlusButton.tap();
await ChannelListScreen.createNewChannelItem.tap();
await CreateOrEditChannelScreen.toBeVisible();
await wait(timeouts.ONE_SEC);
await CreateOrEditChannelScreen.toBeVisible();
const nonLatinChannelName = 'ÁÜ';
await CreateOrEditChannelScreen.displayNameInput.replaceText(nonLatinChannelName);
await wait(timeouts.ONE_SEC);
await CreateOrEditChannelScreen.createButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelScreen.dismissScheduledPostTooltip();
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(nonLatinChannelName);
await ChannelScreen.back();
});
it('MM-T867 - RN apps Copying channel header text', async () => {
// # Navigate to the channel with metadata
await ChannelScreen.open(channelsCategory, channelWithMetadata.name);
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
// * Verify header is visible
await expect(ChannelInfoScreen.extraHeader).toBeVisible();
// # Test long-press and cancel flow
await ChannelInfoScreen.cancelCopyChannelHeader(channelWithMetadata.header);
// * Verify we're still on channel info screen
await expect(ChannelInfoScreen.channelInfoScreen).toBeVisible();
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T865 - RN apps Copying channel purpose text', async () => {
// # Navigate to the channel with metadata
await ChannelScreen.open(channelsCategory, channelWithMetadata.name);
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
// * Verify purpose is visible
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toBeVisible();
// # Test long-press and cancel flow
await ChannelInfoScreen.cancelCopyChannelPurpose(channelWithMetadata.purpose);
// * Verify we're still on channel info screen
await expect(ChannelInfoScreen.channelInfoScreen).toBeVisible();
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
});

View file

@ -0,0 +1,123 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
/**
* Test Cases Included:
* - MM-T3200: RN apps Join existing channel
* - MM-T3202: RN apps Leave public channel
* - MM-T3207: RN apps Leave private channel
* - MM-T3188: RN apps Search for public channel Cancel search join channel
*/
import {Channel, Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {Alert} from '@support/ui/component';
import {
BrowseChannelsScreen,
ChannelInfoScreen,
ChannelListScreen,
ChannelScreen,
LoginScreen,
ServerScreen,
HomeScreen,
} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testUser: any;
let testChannel: any;
let privateChannel: any;
beforeAll(async () => {
const {user, channel, team} = await Setup.apiInit(siteOneUrl);
testUser = user;
testChannel = channel;
const {channel: privateChannelData} = await Channel.apiCreateChannel(siteOneUrl, {type: 'P', prefix: 'channel', teamId: team.id});
privateChannel = privateChannelData;
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, privateChannel.id);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
await HomeScreen.logout();
});
it('MM-T3200 - RN apps Join existing channel', async () => {
await ChannelScreen.open(channelsCategory, testChannel.name);
await wait(timeouts.TWO_SEC);
const joinMessage = `${testUser.username} joined the channel`;
await waitFor(element(by.text(joinMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T3202 - RN apps Leave public channel', async () => {
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelInfoScreen.open();
await ChannelInfoScreen.scrollView.scrollTo('bottom');
await expect(ChannelInfoScreen.leaveChannelOption).toBeVisible();
await ChannelInfoScreen.leaveChannelOption.tap();
await wait(timeouts.ONE_SEC);
const leaveAlertTitle = 'Leave channel';
await expect(element(by.text(leaveAlertTitle))).toBeVisible();
await expect(element(by.text(`Are you sure you want to leave the public channel ${testChannel.display_name}? You can always rejoin.`))).toBeVisible();
await Alert.leaveButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelListScreen.toBeVisible();
});
it('MM-T3207 - RN apps Leave private channel', async () => {
await ChannelScreen.open(channelsCategory, privateChannel.name);
await ChannelInfoScreen.open();
await ChannelInfoScreen.scrollView.scrollTo('bottom');
await expect(ChannelInfoScreen.leaveChannelOption).toBeVisible();
await ChannelInfoScreen.leaveChannelOption.tap();
await wait(timeouts.ONE_SEC);
await expect(element(by.text('Leave channel'))).toBeVisible();
await expect(element(by.text(`Are you sure you want to leave the private channel ${privateChannel.display_name}? You cannot rejoin the channel unless you're invited again.`))).toBeVisible();
await Alert.leaveButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelListScreen.toBeVisible();
});
it('MM-T3188 - RN apps Search for public channel Cancel search join channel', async () => {
const channelName = testChannel.name;
await BrowseChannelsScreen.open();
const searchTerm = channelName.substring(0, 4);
await BrowseChannelsScreen.searchInput.typeText(searchTerm);
await wait(timeouts.TWO_SEC);
await expect(BrowseChannelsScreen.getChannelItemDisplayName(testChannel.name)).toHaveText(testChannel.display_name);
await BrowseChannelsScreen.getChannelItem(testChannel.name).multiTap(2);
await wait(timeouts.ONE_SEC);
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name);
await ChannelScreen.back();
});
});

View file

@ -0,0 +1,332 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
/**
* Test Cases Included:
* - MM-T3195: RN apps Add members to channel
* - MM-T856: Add existing users to public channel from drop-down Add Members
* - MM-T3196: RN apps Manage members in channel
* - MM-T3204: RN apps Add user to private channel
* - MM-T3205: RN apps Remove user from private channel
* - MM-T878: RN apps View Members in GM
*/
import {Channel, Setup, Team, User} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
AddMembersScreen,
ChannelInfoScreen,
ChannelScreen,
CreateDirectMessageScreen,
HomeScreen,
LoginScreen,
ManageChannelMembersScreen,
ServerScreen,
} from '@support/ui/screen';
import {isAndroid, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
// Base setup (shared across all tests)
let testUser: any;
let testTeam: any;
let testChannel: any;
// Test-specific data
let addMemberUser: any; // For MM-T3195
let user2: any; // For MM-T856
let memberUser: any; // For MM-T3196
let privateChannel1: any; // For MM-T3204
let privUser: any; // For MM-T3204
let privateChannel2: any; // For MM-T3205
let removeMeUser: any; // For MM-T3205
let gmUser1: any; // For MM-T878
let gmUser2: any; // For MM-T878
beforeAll(async () => {
// 1. Base setup (shared across all tests)
const {user, team, channel} = await Setup.apiInit(siteOneUrl);
testUser = user;
testTeam = team;
testChannel = channel;
// 2. Test 1 (MM-T3195): User for adding to channel
const {user: newUser1} = await User.apiCreateUser(siteOneUrl, {prefix: 'addmember'});
await Team.apiAddUserToTeam(siteOneUrl, newUser1.id, testTeam.id);
addMemberUser = newUser1;
// 3. Test 2 (MM-T856): Another user for adding to channel
const {user: newUser2} = await User.apiCreateUser(siteOneUrl, {prefix: 'user2'});
await Team.apiAddUserToTeam(siteOneUrl, newUser2.id, testTeam.id);
user2 = newUser2;
// 4. Test 3 (MM-T3196): User already in channel for removal
const {user: newUser3} = await User.apiCreateUser(siteOneUrl, {prefix: 'member'});
await Team.apiAddUserToTeam(siteOneUrl, newUser3.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, newUser3.id, testChannel.id);
memberUser = newUser3;
// 5. Test 4 (MM-T3204): Private channel + user to add
const {channel: privChan1} = await Channel.apiCreateChannel(siteOneUrl, {
teamId: testTeam.id,
type: 'P',
});
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, privChan1.id);
privateChannel1 = privChan1;
const {user: newUser4} = await User.apiCreateUser(siteOneUrl, {prefix: 'privuser'});
await Team.apiAddUserToTeam(siteOneUrl, newUser4.id, testTeam.id);
privUser = newUser4;
// 6. Test 5 (MM-T3205): Private channel + user already in it for removal
const {channel: privChan2} = await Channel.apiCreateChannel(siteOneUrl, {
teamId: testTeam.id,
type: 'P',
});
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, privChan2.id);
privateChannel2 = privChan2;
const {user: newUser5} = await User.apiCreateUser(siteOneUrl, {prefix: 'removeme'});
await Team.apiAddUserToTeam(siteOneUrl, newUser5.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, newUser5.id, privChan2.id);
removeMeUser = newUser5;
// 7. Test 6 (MM-T878): Two users for GM creation
const {user: gmUserOne} = await User.apiCreateUser(siteOneUrl, {prefix: 'gmuser1'});
await wait(timeouts.ONE_SEC);
const {user: gmUserTwo} = await User.apiCreateUser(siteOneUrl, {prefix: 'gmuser2'});
await wait(timeouts.ONE_SEC);
await Team.apiAddUserToTeam(siteOneUrl, gmUserOne.id, testTeam.id);
await wait(timeouts.ONE_SEC);
await Team.apiAddUserToTeam(siteOneUrl, gmUserTwo.id, testTeam.id);
await wait(timeouts.ONE_SEC);
gmUser1 = gmUserOne;
gmUser2 = gmUserTwo;
// 8. Login once with test user
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
// # Log out
await HomeScreen.logout();
});
it('MM-T3195 - RN apps Add members to channel', async () => {
// # Use pre-created user
const newUser = addMemberUser;
// # Open default test channel
await ChannelScreen.open(channelsCategory, testChannel.name);
// # Open channel info and tap add members
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.addMembersAction).toBeVisible();
await ChannelInfoScreen.addMembersAction.tap();
await wait(timeouts.TWO_SEC);
// # Dismiss tutorial if present
await AddMembersScreen.dismissTutorial();
await AddMembersScreen.toBeVisible();
// # Search and add user
await AddMembersScreen.searchAndAddUser(newUser.username, newUser.id);
// * Verify user added system message appears
await ChannelScreen.toBeVisible();
await wait(timeouts.TWO_SEC);
const systemMessage = `${newUser.username} added to the channel by ${testUser.username}`;
await waitFor(element(by.text(systemMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T856 - Add existing users to public channel from drop-down Add Members', async () => {
// # Use pre-created user
const newUser = user2;
// # Open default test channel
await ChannelScreen.open(channelsCategory, testChannel.name);
// # Open channel info and tap add members
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.addMembersAction).toBeVisible();
await ChannelInfoScreen.addMembersAction.tap();
await wait(timeouts.TWO_SEC);
// # Dismiss tutorial if present and search and add user
await AddMembersScreen.dismissTutorial();
await AddMembersScreen.toBeVisible();
await AddMembersScreen.searchAndAddUser(newUser.username, newUser.id);
// * Verify user added system message appears
await ChannelScreen.toBeVisible();
await wait(timeouts.TWO_SEC);
const systemMessage = `${newUser.username} added to the channel by ${testUser.username}`;
await waitFor(element(by.text(systemMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T3196 - RN apps Manage members in channel', async () => {
// # Use pre-created user (already in channel)
const removedUser = memberUser;
// # Open default test channel
await ChannelScreen.open(channelsCategory, testChannel.name);
// # Open channel info and tap members option
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.membersOption).toBeVisible();
await ChannelInfoScreen.membersOption.tap();
await wait(timeouts.TWO_SEC);
await element(by.text(isAndroid()? 'MANAGE': 'Manage')).tap();
await wait(timeouts.TWO_SEC);
// # Search and remove user
await ManageChannelMembersScreen.searchAndRemoveUser(removedUser.username, removedUser.id);
// * Verify user removed system message appears
await ChannelInfoScreen.close();
await ChannelScreen.toBeVisible();
await wait(timeouts.TWO_SEC);
const systemMessage = `${removedUser.username} was removed from the channel`;
await waitFor(element(by.text(systemMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T3204 - RN apps Add user to private channel', async () => {
// # Use pre-created private channel and user
const privateChannel = privateChannel1;
const newUser = privUser;
// # Open private channel
await ChannelScreen.open(channelsCategory, privateChannel.name);
// # Open channel info and tap add members
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.addMembersAction).toBeVisible();
await ChannelInfoScreen.addMembersAction.tap();
await wait(timeouts.TWO_SEC);
// # Dismiss tutorial if present and search and add user
await AddMembersScreen.dismissTutorial();
await AddMembersScreen.toBeVisible();
await AddMembersScreen.searchAndAddUser(newUser.username, newUser.id);
// * Verify user added system message appears
await ChannelScreen.toBeVisible();
await wait(timeouts.TWO_SEC);
const systemMessage = `${newUser.username} added to the channel by ${testUser.username}`;
await waitFor(element(by.text(systemMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T3205 - RN apps Remove user from private channel', async () => {
// # Use pre-created private channel and user (already in channel)
const privateChannel = privateChannel2;
const removedUser = removeMeUser;
// # Open private channel
await ChannelScreen.open(channelsCategory, privateChannel.name);
// # Open channel info and tap members option
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.membersOption).toBeVisible();
await ChannelInfoScreen.membersOption.tap();
await wait(timeouts.TWO_SEC);
await element(by.text(isAndroid()? 'MANAGE': 'Manage')).tap();
await wait(timeouts.TWO_SEC);
// # Search and remove user
await ManageChannelMembersScreen.searchAndRemoveUser(removedUser.username, removedUser.id);
// * Verify user removed system message appears
await ChannelInfoScreen.close();
await ChannelScreen.toBeVisible();
await wait(timeouts.TWO_SEC);
const systemMessage = `${removedUser.username} was removed from the channel`;
await waitFor(element(by.text(systemMessage).withAncestor(by.id('post_list')))).
toBeVisible();
await ChannelScreen.back();
});
it('MM-T878 - RN apps View Members in GM', async () => {
await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.searchInput.replaceText(`${gmUser1.username}`);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.getUserItem(gmUser1.id).tap();
// * Verify the first new user is selected
await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(gmUser1.id)).toBeVisible();
// # Search for the second new user and tap on the second new user item
await CreateDirectMessageScreen.searchInput.replaceText(`${gmUser2.username}`);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.getUserItem(gmUser2.id).tap();
// * Verify the second new user is selected
await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(gmUser2.id)).toBeVisible();
// # Tap on start button
await CreateDirectMessageScreen.startButton.tap();
await ChannelScreen.dismissScheduledPostTooltip();
await ChannelScreen.toBeVisible();
// # Open channel info and tap members option
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.membersOption).toBeVisible();
await ChannelInfoScreen.membersOption.tap();
await wait(timeouts.TWO_SEC);
// * Verify members list is visible
await expect(ManageChannelMembersScreen.gmMemberSectionList).toBeVisible();
// # Go back
await ManageChannelMembersScreen.backButton.tap();
await wait(timeouts.ONE_SEC);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
});

View file

@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {Channel, Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelInfoScreen,
ChannelListScreen,
ChannelScreen,
FindChannelsScreen,
LoginScreen,
ServerScreen,
HomeScreen,
} from '@support/ui/screen';
import {getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
const favoritesCategory = 'favorites';
let testUser: any;
let testTeam: any;
let testChannel: any;
let archiveChannel: any;
beforeAll(async () => {
const {user, channel, team} = await Setup.apiInit(siteOneUrl);
testUser = user;
testChannel = channel;
testTeam = team;
// Create a separate channel for archiving test
const {channel: archiveCh} = await Channel.apiCreateChannel(siteOneUrl, {
teamId: testTeam.id,
name: `archive-test-${getRandomId()}`,
displayName: `Archive Test ${getRandomId()}`,
type: 'O',
});
archiveChannel = archiveCh;
await wait(timeouts.THREE_SEC);
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, archiveChannel.id);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
await HomeScreen.logout();
});
it('MM-T3189 - RN apps Display channel list', async () => {
await expect(ChannelListScreen.channelListScreen).toBeVisible();
await wait(timeouts.ONE_SEC);
});
it('MM-T3190 - RN apps Display Channel Info', async () => {
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.toBeVisible();
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.channelInfoScreen).toBeVisible();
await wait(timeouts.ONE_SEC);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
it('MM-T3191 - RN apps Change channel', async () => {
await FindChannelsScreen.open();
await wait(timeouts.ONE_SEC);
const searchTerm = testChannel.name.substring(0, 3);
await FindChannelsScreen.searchInput.typeText(searchTerm);
await wait(timeouts.TWO_SEC);
await expect(FindChannelsScreen.getFilteredChannelItem(testChannel.name)).toBeVisible();
await FindChannelsScreen.getFilteredChannelItem(testChannel.name).tap();
await wait(timeouts.ONE_SEC);
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name);
await ChannelScreen.back();
});
it('MM-T850 - RN apps Favorite a channel', async () => {
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelInfoScreen.open();
await expect(ChannelInfoScreen.favoriteAction).toBeVisible();
await ChannelInfoScreen.favoriteAction.tap();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.unfavoriteAction).toBeVisible();
await ChannelInfoScreen.close();
await ChannelScreen.back();
// * Verify channel is listed under favorites category
await expect(ChannelListScreen.getChannelItemDisplayName(favoritesCategory, testChannel.name)).toBeVisible();
});
it('MM-T3192 - RN apps Un-favorite a channel', async () => {
await ChannelScreen.open(favoritesCategory, testChannel.name);
await ChannelInfoScreen.open();
await expect(ChannelInfoScreen.unfavoriteAction).toBeVisible();
await ChannelInfoScreen.unfavoriteAction.tap();
await wait(timeouts.THREE_SEC);
await expect(ChannelInfoScreen.favoriteAction).toBeVisible();
await ChannelInfoScreen.close();
await ChannelScreen.back();
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name)).toBeVisible();
});
it('MM-T3197 - RN apps Archive public or private channel', async () => {
// # Navigate to the archive channel
await ChannelScreen.open(channelsCategory, archiveChannel.name);
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
// # Scroll to bottom to reveal archive option
await ChannelInfoScreen.scrollView.scrollTo('bottom');
await wait(timeouts.ONE_SEC);
// # Archive the channel
await ChannelInfoScreen.archivePublicChannel({confirm: true});
// * Verify channel info screen is closed
await wait(timeouts.TWO_SEC);
await expect(ChannelInfoScreen.channelInfoScreen).not.toBeVisible();
// * Verify we're back at channel list
await ChannelListScreen.toBeVisible();
// * Verify archived channel is not visible in the list
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, archiveChannel.name)).not.toBeVisible();
});
});

View file

@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {Setup} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelInfoScreen,
ChannelScreen,
LoginScreen,
ServerScreen,
HomeScreen,
} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testUser: any;
let testChannel: any;
beforeAll(async () => {
const {user, channel} = await Setup.apiInit(siteOneUrl);
testUser = user;
testChannel = channel;
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
await HomeScreen.logout();
});
it('MM-T3198 - Channel notifications Mobile Push', async () => {
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelInfoScreen.open();
await wait(timeouts.ONE_SEC);
await expect(ChannelInfoScreen.notificationPreferenceOption).toBeVisible();
await ChannelInfoScreen.notificationPreferenceOption.tap();
await wait(timeouts.TWO_SEC);
const notificationSettingsScreen = element(by.id('push_notification_settings.screen'));
await expect(notificationSettingsScreen).toBeVisible();
const backButton = element(by.id('screen.back.button'));
await backButton.tap();
await wait(timeouts.ONE_SEC);
await ChannelInfoScreen.close();
await ChannelScreen.back();
});
});

View file

@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *******************************************************************
// - [#] indicates a test step (e.g. # Go to a screen)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
/**
* Test Cases Included:
* - MM-T858: Combined joinleave messages in public channel
*/
import {Channel, Post, Setup, Team, User} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelScreen,
LoginScreen,
ServerScreen,
HomeScreen,
} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Channels', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testUser: any;
let testTeam: any;
let testChannel: any;
let user1: any;
let user5: any;
beforeAll(async () => {
const {user, channel, team} = await Setup.apiInit(siteOneUrl);
testUser = user;
testTeam = team;
testChannel = channel;
({user: user1} = await User.apiCreateUser(siteOneUrl, {prefix: 'user1'}));
const {user: user2} = await User.apiCreateUser(siteOneUrl, {prefix: 'user2'});
const {user: user3} = await User.apiCreateUser(siteOneUrl, {prefix: 'user3'});
await Team.apiAddUserToTeam(siteOneUrl, user1.id, testTeam.id);
await Team.apiAddUserToTeam(siteOneUrl, user2.id, testTeam.id);
await Team.apiAddUserToTeam(siteOneUrl, user3.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, user1.id, testChannel.id);
await Channel.apiAddUserToChannel(siteOneUrl, user2.id, testChannel.id);
await Channel.apiAddUserToChannel(siteOneUrl, user3.id, testChannel.id);
await wait(timeouts.TWO_SEC);
await Channel.apiRemoveUserFromChannel(siteOneUrl, testChannel.id, user1.id);
await wait(timeouts.TWO_SEC);
const {user: user4} = await User.apiCreateUser(siteOneUrl, {prefix: 'user4'});
await Team.apiAddUserToTeam(siteOneUrl, user4.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, user4.id, testChannel.id);
await wait(timeouts.TWO_SEC);
await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id,
message: 'Test message to interrupt system messages',
});
await wait(timeouts.TWO_SEC);
({user: user5} = await User.apiCreateUser(siteOneUrl, {prefix: 'user5'}));
await Team.apiAddUserToTeam(siteOneUrl, user5.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, user5.id, testChannel.id);
await wait(timeouts.TWO_SEC);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
afterAll(async () => {
await HomeScreen.logout();
});
it('MM-T858 - Combined joinleave messages in public channel', async () => {
// Verify the posts via API to ensure correct structure
const {posts} = await Post.apiGetPostsInChannel(siteOneUrl, testChannel.id);
// Verify we have the expected post types (no expect needed)
const hasJoinPost = posts.some((p: { type: string }) => p.type === 'system_join_channel');
const hasAddPosts = posts.filter((p: { type: string }) => p.type === 'system_add_to_channel').length >= 4;
const hasRemovePost = posts.some((p: { type: string }) => p.type === 'system_remove_from_channel');
const hasRegularPost = posts.some((p: { message: string }) => p.message === 'Test message to interrupt system messages');
if (!hasJoinPost || !hasAddPosts || !hasRemovePost || !hasRegularPost) {
throw new Error(`Missing expected posts. Join: ${hasJoinPost}, Adds: ${hasAddPosts}, Remove: ${hasRemovePost}, Regular: ${hasRegularPost}`);
}
// Now verify the UI displays these messages correctly
await ChannelScreen.open(channelsCategory, testChannel.name);
await wait(timeouts.TWO_SEC);
// Verify the regular message is visible
await expect(element(by.text('Test message to interrupt system messages'))).toBeVisible();
await ChannelScreen.back();
});
});

View file

@ -93,7 +93,6 @@ describe('Channels - Channel Post List', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open('channels', testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);

View file

@ -23,7 +23,6 @@ import {
LoginScreen,
ServerScreen,
} from '@support/ui/screen';
import channelScreen from '@support/ui/screen/channel';
import {timeouts, wait} from '@support/utils';
import {expect, waitFor} from 'detox';
@ -62,7 +61,6 @@ describe('Messaging - Channel Link', () => {
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, targetChannel.id);
const channelLink = `${serverOneUrl}/${testTeam.name}/channels/${targetChannel.name}`;
await ChannelScreen.postMessage(channelLink);
await channelScreen.dismissKeyboard();
// # Tap on channel link
await element(by.text(channelLink)).tap();
@ -82,7 +80,6 @@ describe('Messaging - Channel Link', () => {
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, targetChannel.id);
const channelLink = `${serverOneUrl}/${testTeam.name}/channels/${targetChannel.name}`;
await ChannelScreen.postMessage(channelLink);
await channelScreen.dismissKeyboard();
// # Wait for keyboard to dismiss and message to be visible
await wait(timeouts.TWO_SEC);

View file

@ -60,7 +60,6 @@ describe('Messaging - Emojis and Reactions', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openPostOptionsFor(post.id, message);
@ -105,7 +104,6 @@ describe('Messaging - Emojis and Reactions', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openPostOptionsFor(post.id, message);
await EmojiPickerScreen.open();
@ -140,7 +138,6 @@ describe('Messaging - Emojis and Reactions', () => {
const message = 'brown fox :fox_face: lazy dog :dog:';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is posted with emojis
const resolvedMessage = 'brown fox 🦊 lazy dog 🐶';
@ -174,7 +171,6 @@ describe('Messaging - Emojis and Reactions', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const searchTerm = 'blahblahblahblah';
await ChannelScreen.openPostOptionsFor(post.id, message);

View file

@ -66,7 +66,6 @@ describe('Messaging - Follow and Unfollow Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// # Wait for keyboard to dismiss and message to be visible
await wait(timeouts.ONE_SEC);
@ -106,7 +105,6 @@ describe('Messaging - Follow and Unfollow Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// # Wait for keyboard to dismiss and message to be visible
await wait(timeouts.ONE_SEC);

View file

@ -56,7 +56,6 @@ describe('Messaging - Message Delete', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -78,7 +77,6 @@ describe('Messaging - Message Delete', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -100,7 +98,6 @@ describe('Messaging - Message Delete', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: parentPostListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, message);
@ -114,7 +111,6 @@ describe('Messaging - Message Delete', () => {
// # Post a reply, open post options for the reply message, tap delete option and confirm
const replyMessage = `${message} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
const {post: replyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: replyPostListPostItem} = ThreadScreen.getPostListPostItem(replyPost.id, replyMessage);

View file

@ -57,7 +57,6 @@ describe('Messaging - Message Edit', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -95,7 +94,6 @@ describe('Messaging - Message Edit', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -129,7 +127,6 @@ describe('Messaging - Message Edit', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: parentPostListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, message);
@ -143,7 +140,6 @@ describe('Messaging - Message Edit', () => {
// # Post a reply, open post options for the reply message and tap edit option
const replyMessage = `${message} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
const {post: replyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: replyPostListPostItem} = ThreadScreen.getPostListPostItem(replyPost.id, replyMessage);

View file

@ -61,7 +61,6 @@ describe('Messaging - Message Reply', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -80,7 +79,6 @@ describe('Messaging - Message Reply', () => {
// # Reply to parent post
const replyMessage = `${message} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
// * Verify reply message is posted
const {post: replyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -96,7 +94,6 @@ describe('Messaging - Message Reply', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -118,7 +115,6 @@ describe('Messaging - Message Reply', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message);

View file

@ -57,7 +57,6 @@ describe('Messaging - Pin and Unpin Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is posted
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -90,7 +89,7 @@ describe('Messaging - Pin and Unpin Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message);
await expect(postListPostItem).toBeVisible();

View file

@ -57,7 +57,6 @@ describe('Messaging - Save and Unsave Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
// * Verify message is posted
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -90,7 +89,6 @@ describe('Messaging - Save and Unsave Message', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, message);

View file

@ -73,7 +73,6 @@ describe('Search - Pinned Messages', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: channelPostItem} = ChannelScreen.getPostListPostItem(post.id, message);
@ -115,7 +114,6 @@ describe('Search - Pinned Messages', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post: pinnedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: channelPostItem} = ChannelScreen.getPostListPostItem(pinnedPost.id, message);
@ -156,7 +154,6 @@ describe('Search - Pinned Messages', () => {
// # Post a reply
const replyMessage = `${updatedMessage} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
// * Verify reply is posted
const {post: replyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -189,7 +186,6 @@ describe('Search - Pinned Messages', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post: pinnedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: channelPostItem} = ChannelScreen.getPostListPostItem(pinnedPost.id, message);
@ -223,7 +219,6 @@ describe('Search - Pinned Messages', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(message);
await ChannelScreen.dismissKeyboard();
const {post: pinnedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem: channelPostItem} = ChannelScreen.getPostListPostItem(pinnedPost.id, message);

View file

@ -58,7 +58,6 @@ describe('Threads - Follow and Unfollow Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.FOUR_SEC);
@ -93,7 +92,6 @@ describe('Threads - Follow and Unfollow Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
await ThreadScreen.postMessage(`${parentMessage} reply`);
@ -126,7 +124,6 @@ describe('Threads - Follow and Unfollow Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
await ThreadScreen.postMessage(`${parentMessage} reply`);
@ -175,7 +172,6 @@ describe('Threads - Follow and Unfollow Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
const replyMessage = `${parentMessage} reply`;

View file

@ -72,7 +72,6 @@ describe('Threads - Global Threads', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);

View file

@ -58,7 +58,6 @@ describe('Threads - Open Thread in Channel', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
await ChannelScreen.dismissScheduledPostTooltip();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
@ -100,7 +99,6 @@ describe('Threads - Open Thread in Channel', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.FOUR_SEC);

View file

@ -57,7 +57,6 @@ describe('Threads - Reply to Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
@ -66,7 +65,6 @@ describe('Threads - Reply to Thread', () => {
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
const replyMessage = `${parentMessage} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
await ThreadScreen.back();
await ChannelScreen.back();
await GlobalThreadsScreen.open();
@ -84,7 +82,6 @@ describe('Threads - Reply to Thread', () => {
// # Add new reply to thread
const newReplyMessage = `${parentMessage} new reply`;
await ThreadScreen.postMessage(newReplyMessage);
await ChannelScreen.dismissKeyboard();
// * Verify new reply is posted
const {post: newReplyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -101,14 +98,12 @@ describe('Threads - Reply to Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
await waitFor(postListPostItem).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
const replyMessage = `${parentMessage} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
await ThreadScreen.back();
await ChannelScreen.back();
@ -126,7 +121,6 @@ describe('Threads - Reply to Thread', () => {
// # Add new reply to thread
const newReplyMessage = `${parentMessage} new reply`;
await ThreadScreen.postMessage(newReplyMessage);
await ChannelScreen.dismissKeyboard();
// * Verify new reply is posted
const {post: newReplyPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);

View file

@ -58,7 +58,6 @@ describe('Threads - Save and Unsave Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(parentPost.id, parentMessage);
@ -67,7 +66,6 @@ describe('Threads - Save and Unsave Thread', () => {
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
const replyMessage = `${parentMessage} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
await ThreadScreen.back();
await ChannelScreen.back();
await GlobalThreadsScreen.open();
@ -103,12 +101,10 @@ describe('Threads - Save and Unsave Thread', () => {
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage(parentMessage);
await ChannelScreen.dismissKeyboard();
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
const replyMessage = `${parentMessage} reply`;
await ThreadScreen.postMessage(replyMessage);
await ChannelScreen.dismissKeyboard();
await ThreadScreen.back();
await ChannelScreen.back();
await GlobalThreadsScreen.open();

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
/* eslint-disable no-await-in-loop, no-console */
import {cleanupAdbReversePorts, resetAdbServer} from '@support/adb_utils';
import {ClaudePromptHandler} from '@support/pilot/ClaudePromptHandler';
import {Plugin, System, User} from '@support/server_api';
import {siteOneUrl} from '@support/test_config';
@ -111,40 +110,19 @@ export async function launchAppWithRetry(): Promise<void> {
}
console.info(`✅ App launched successfully on attempt ${attempt}`);
return; // Success, exit the function
return;
} catch (error) {
lastError = error;
console.warn(`❌ App launch failed on attempt ${attempt}/${MAX_RETRY_ATTEMPTS}: ${(error as Error).message}`);
// If this is the last attempt, don't wait
if (attempt < MAX_RETRY_ATTEMPTS) {
// Check if this is an ADB port binding issue (Android-specific, common in CI)
const errorMessage = (error as Error).message;
const isAdbPortError = errorMessage.includes('cannot bind listener') ||
errorMessage.includes('Address already in use') ||
errorMessage.includes('adb');
// Only attempt ADB cleanup if we detect an ADB-related error
// These functions are safe no-ops on iOS
if (isAdbPortError) {
console.warn('Detected ADB-related error, attempting cleanup...');
await cleanupAdbReversePorts();
// On second retry with port issues, also reset ADB server
if (attempt >= 2) {
console.warn('Multiple failures detected, attempting ADB server reset...');
await resetAdbServer();
}
}
console.warn(`Waiting ${RETRY_DELAY}ms before retrying...`);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
}
}
}
// If we get here, all attempts failed
throw new Error(`Failed to launch app after ${MAX_RETRY_ATTEMPTS} attempts. Last error: ${(lastError as Error).message}`);
}
@ -153,28 +131,20 @@ export async function launchAppWithRetry(): Promise<void> {
* @returns {Promise<void>}
*/
async function initializeClaudePromptHandler(): Promise<void> {
if (process.env.ANTHROPIC_API_KEY) {
try {
if (!process.env.ANTHROPIC_API_KEY) {
return;
}
const promptHandler = new ClaudePromptHandler(process.env.ANTHROPIC_API_KEY);
pilot.init(promptHandler);
} else {
console.info('To use ClaudePromptHandler, please set the ANTHROPIC_API_KEY environment variable.');
} catch (e) {
console.warn('Claude init failed, continuing without AI:', e);
}
}
beforeAll(async () => {
// Reset flag to ensure each test file starts with a clean app launch
isFirstLaunch = true;
await initializeClaudePromptHandler();
// Clean up any stale ADB reverse port mappings from previous runs
// This is crucial in CI where crashed tests may leave stale connections
await cleanupAdbReversePorts();
// Login as sysadmin and reset server configuration
await System.apiCheckSystemHealth(siteOneUrl);
await User.apiAdminLogin(siteOneUrl);
await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl);
await launchAppWithRetry();
// Verify Detox connection is healthy after app launch
@ -182,28 +152,10 @@ beforeAll(async () => {
// Wait for app to be fully ready (database initialized, bridge ready)
await waitForAppReady();
});
// Add this to speed up test cleanup
afterAll(async () => {
try {
// Dismiss keyboard if visible to prevent session invalidation issues
try {
await device.sendToHome();
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
console.warn('[Teardown] Could not send app to home:', error);
}
await device.terminateApp();
} catch (error) {
console.error('[Teardown] Error terminating app:', error);
}
// Clean up ADB connections after tests complete
try {
await cleanupAdbReversePorts();
} catch (error) {
console.error('[Teardown] Error cleaning up ADB ports:', error);
}
await initializeClaudePromptHandler();
// Login as sysadmin and reset server configuration
await System.apiCheckSystemHealth(siteOneUrl);
await User.apiAdminLogin(siteOneUrl);
await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl);
});

1637
detox/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",
"devDependencies": {
"@aws-sdk/client-s3": "3.712.0",
"@aws-sdk/lib-storage": "3.712.0",
"@aws-sdk/client-s3": "3.937.0",
"@aws-sdk/lib-storage": "3.937.0",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-transform-modules-commonjs": "7.26.3",
"@babel/plugin-transform-runtime": "7.25.9",
@ -23,7 +23,7 @@
"babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3",
"deepmerge": "4.3.1",
"detox": "20.46.3",
"detox": "20.47.0",
"dotenv": "17.2.1",
"form-data": "4.0.1",
"jest": "29.7.0",
@ -48,11 +48,11 @@
"e2e:android-inject-settings": "node inject-detox-settings.js",
"e2e:android-create-emulator": "./create_android_emulator.sh",
"e2e:android-build": "detox build -c android.emu.debug",
"e2e:android-test": "detox test -c android.emu.debug --reuse --record-logs failing --take-screenshots failing",
"e2e:android-test": "detox test -c android.emu.debug --record-logs failing --take-screenshots failing",
"e2e:android-build-release": "detox build -c android.emu.release",
"e2e:android-test-release": "detox test -c android.emu.release --record-logs failing --take-screenshots failing",
"e2e:ios-build": "IOS=true detox build -c ios.sim.debug",
"e2e:ios-test": "IOS=true detox test -c ios.sim.debug --reuse --record-logs failing --take-screenshots failing",
"e2e:ios-test": "IOS=true detox test -c ios.sim.debug --record-logs failing --take-screenshots failing",
"e2e:ios-build-release": "detox build -c ios.sim.release",
"e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing",
"e2e:ios-disable-autofill": "cd utils && node disable_ios_autofill.js",