iOS & Android: Migrate 29 Channel Settings tests from Rainforest to Detox (#9313)
This commit is contained in:
parent
121980c7e5
commit
95d50125e4
44 changed files with 2088 additions and 1112 deletions
10
.github/workflows/e2e-ios-template.yml
vendored
10
.github/workflows/e2e-ios-template.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -83,10 +83,10 @@
|
|||
"behavior": {
|
||||
"init": {
|
||||
"reinstallApp": false,
|
||||
"launchApp": true
|
||||
"launchApp": false
|
||||
},
|
||||
"cleanup": {
|
||||
"shutdownDevice": true
|
||||
"shutdownDevice": false
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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: '.',
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
77
detox/e2e/support/ui/screen/add_members.ts
Normal file
77
detox/e2e/support/ui/screen/add_members.ts
Normal 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;
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
1637
detox/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue