E2E feature schedule posts (#8709)

- Fix iOS and Android CI.
- Fixes E2E tests on both platform
- Added tests for scheduled draft feature.
This commit is contained in:
yasser khan 2025-04-23 12:10:37 +05:30 committed by GitHub
parent 11118c43c5
commit 3cbde1663d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 1509 additions and 278 deletions

View file

@ -52,7 +52,7 @@ on:
description: "Android API level" description: "Android API level"
required: false required: false
type: string type: string
default: "33" default: "34"
outputs: outputs:
STATUS: STATUS:
value: ${{ jobs.generate-report.outputs.STATUS }} value: ${{ jobs.generate-report.outputs.STATUS }}
@ -130,7 +130,7 @@ jobs:
name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }} name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
continue-on-error: true continue-on-error: true
timeout-minutes: 150 timeout-minutes: 240
env: env:
ANDROID_HOME: /usr/local/lib/android/sdk ANDROID_HOME: /usr/local/lib/android/sdk
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
@ -170,6 +170,12 @@ jobs:
cd detox cd detox
npm install npm install
- name: Set .env with RUNNING_E2E=true
run: |
cat > .env <<EOF
RUNNING_E2E=true
EOF
- name: Create destination path - name: Create destination path
run: mkdir -p android/app/build run: mkdir -p android/app/build
@ -194,18 +200,29 @@ jobs:
- name: Accept Android licenses - name: Accept Android licenses
run: | run: |
yes | sdkmanager --licenses || true mkdir -p ~/.android
yes | sdkmanager --licenses > sdk_licenses_output.txt || true
# Check if "All SDK package licenses accepted" appears in output
if grep -q "All SDK package licenses accepted" sdk_licenses_output.txt; then
echo "✅ All licenses accepted successfully."
else
echo "❌ Licenses not fully accepted."
cat sdk_licenses_output.txt
exit 1
fi
- name: Install Android system image - name: Install Android SDK components
run: | run: |
sdkmanager "system-images;android-34;default;x86_64" yes | sdkmanager --install "platform-tools" "emulator" "platforms;android-34" "system-images;android-34;default;x86_64" "system-images;android-34;google_apis;x86_64"
sdkmanager "platform-tools" "emulator" env:
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
- name: Create and run Android Emulator - name: Create and run Android Emulator
run: | run: |
cd detox cd detox
chmod +x ./create_android_emulator.sh chmod +x ./create_android_emulator.sh
./create_android_emulator.sh ${{ env.SDK_VERSION }} ${{ env.AVD_NAME }} ${{ matrix.specs }} CI=true ./create_android_emulator.sh ${{ env.SDK_VERSION }} ${{ env.AVD_NAME }} ${{ matrix.specs }}
continue-on-error: true # We want to run all the tests continue-on-error: true # We want to run all the tests
- name: Upload Android Test Report - name: Upload Android Test Report
@ -293,3 +310,36 @@ jobs:
echo "" >> ${GITHUB_STEP_SUMMARY} echo "" >> ${GITHUB_STEP_SUMMARY}
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY} echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY} echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}
- name: Comment report on the PR
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
script: |
const prNumber = context.payload.pull_request.number;
const commentBody = `**Android E2E Test Report**: ${process.env.MOBILE_SHA} | ${process.env.PERCENTAGE}% (${process.env.PASSES}/${process.env.TOTAL}) | [full report](${process.env.TARGET_URL})
| Tests | Passed ✅ | Failed ❌ | Skipped ⏭️ | Errors ⚠️ |
|:---:|:---:|:---:|:---:|:---:|
| ${process.env.TOTAL} | ${process.env.PASSES} | ${process.env.FAILURES} | ${process.env.SKIPPED} | ${process.env.ERRORS} |
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody,
});
env:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
PASSES: ${{ steps.summary.outputs.PASSES }}
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
ERRORS: ${{ steps.summary.outputs.ERRORS }}
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
RUN_TYPE: ${{ inputs.run-type }}
MOBILE_REF: ${{ needs.generate-specs.outputs.mobile_ref }}
MOBILE_SHA: ${{ needs.generate-specs.outputs.mobile_sha }}
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}

View file

@ -6,6 +6,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- feature_schedule_posts
types: types:
- labeled - labeled
@ -104,6 +105,7 @@ jobs:
cd detox cd detox
npm install npm install
npm install -g detox-cli npm install -g detox-cli
npm run e2e:android-inject-settings
npm run e2e:android-build npm run e2e:android-build
- name: Upload Android Build - name: Upload Android Build

View file

@ -38,12 +38,12 @@ on:
description: "iPhone simulator name" description: "iPhone simulator name"
required: false required: false
type: string type: string
default: "iPhone 16 Pro" default: "iPhone 15 Pro"
ios_device_os_name: ios_device_os_name:
description: "iPhone simulator OS version" description: "iPhone simulator OS version"
required: false required: false
type: string type: string
default: "iOS 18.1" default: "iOS 17.2"
low_bandwidth_mode: low_bandwidth_mode:
description: "Enable low bandwidth mode" description: "Enable low bandwidth mode"
required: false required: false
@ -81,7 +81,7 @@ env:
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
FAILURE_MESSAGE: "Something has failed" FAILURE_MESSAGE: "Something has failed"
IOS: "true" IOS: "true"
RUNNING_E2E: true RUNNING_E2E: "true"
jobs: jobs:
generate-specs: generate-specs:
@ -124,7 +124,7 @@ jobs:
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }} name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
runs-on: macos-14 runs-on: macos-14
continue-on-error: true continue-on-error: true
timeout-minutes: ${{ inputs.low_bandwidth_mode && 140 || 70 }} timeout-minutes: ${{ inputs.low_bandwidth_mode && 240 || 180 }}
env: env:
IOS: true IOS: true
needs: needs:
@ -153,7 +153,10 @@ jobs:
path: mobile-artifacts path: mobile-artifacts
- name: Unzip iOS Simulator Build - name: Unzip iOS Simulator Build
run: unzip -o mobile-artifacts/*.zip -d mobile-artifacts/ run: |
unzip -o mobile-artifacts/*.zip -d mobile-artifacts/
# delete zip file
rm -f mobile-artifacts/*.zip
- name: Prepare Low Bandwidth Environment - name: Prepare Low Bandwidth Environment
id: prepare-low-bandwidth id: prepare-low-bandwidth
@ -167,9 +170,6 @@ jobs:
upload_speed: "3300" upload_speed: "3300"
latency: "500" latency: "500"
- name: Start React Native Metro Server
run: npm run start &
- name: Install Detox Dependencies - name: Install Detox Dependencies
run: cd detox && npm i run: cd detox && npm i
@ -180,13 +180,25 @@ jobs:
with: with:
test_server_url: ${{ env.SITE_1_URL }} test_server_url: ${{ env.SITE_1_URL }}
- name: Set .env with RUNNING_E2E=true
run: |
cat > .env <<EOF
echo "RUNNING_E2E=true" >> .env
- name: Run Detox E2E Tests - name: Run Detox E2E Tests
continue-on-error: true # We want to run all the tests continue-on-error: true # We want to run all the tests
run: | run: |
# Start the server
npm run start &
sleep 120 # Wait for watchman to finish querying the files
cd detox cd detox
npm run clean-detox
npm run detox:config-gen npm run detox:config-gen
npm run e2e:ios-test -- ${{ matrix.specs }} npm run e2e:ios-test -- ${{ matrix.specs }}
env: env:
DETOX_DISABLE_HIERARCHY_DUMP: "YES"
DETOX_DISABLE_SCREENSHOT_TRACKING: "YES"
DETOX_LOGLEVEL: "debug"
DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }} DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }}
DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }} DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }}
LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }} LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }}
@ -295,3 +307,36 @@ jobs:
echo "" >> ${GITHUB_STEP_SUMMARY} echo "" >> ${GITHUB_STEP_SUMMARY}
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY} echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY} echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}
- name: Comment report on the PR
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
script: |
const prNumber = context.payload.pull_request.number;
const commentBody = `**iOS E2E Test Report**: ${process.env.MOBILE_SHA} | ${process.env.PERCENTAGE}% (${process.env.PASSES}/${process.env.TOTAL}) | [full report](${process.env.TARGET_URL})
| Tests | Passed ✅ | Failed ❌ | Skipped ⏭️ | Errors ⚠️ |
|:---:|:---:|:---:|:---:|:---:|
| ${process.env.TOTAL} | ${process.env.PASSES} | ${process.env.FAILURES} | ${process.env.SKIPPED} | ${process.env.ERRORS} |
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody,
});
env:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
PASSES: ${{ steps.summary.outputs.PASSES }}
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
ERRORS: ${{ steps.summary.outputs.ERRORS }}
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
RUN_TYPE: ${{ inputs.run-type }}
MOBILE_REF: ${{ needs.generate-specs.outputs.mobile_ref }}
MOBILE_SHA: ${{ needs.generate-specs.outputs.mobile_sha }}
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}

View file

@ -137,7 +137,10 @@ const DraftAndScheduledPostHeader: React.FC<Props> = ({
return ( return (
<View style={style.scheduledContainer}> <View style={style.scheduledContainer}>
<Text style={style.scheduledAtText}> <Text
style={style.scheduledAtText}
testID='scheduled_post_header.scheduled_at'
>
{isSent? intl.formatMessage({id: 'scheduled_post.header.sent', defaultMessage: 'Sent'}): intl.formatMessage( {isSent? intl.formatMessage({id: 'scheduled_post.header.sent', defaultMessage: 'Sent'}): intl.formatMessage(
{id: 'channel_info.scheduled', defaultMessage: 'Send on {time}'}, {id: 'channel_info.scheduled', defaultMessage: 'Send on {time}'},
{time: scheduledTime}, {time: scheduledTime},

View file

@ -88,7 +88,10 @@ function ScheduledPostIndicator({
name='clock-send-outline' name='clock-send-outline'
size={18} size={18}
/> />
<Text style={styles.text}> <Text
style={styles.text}
testID='scheduled_post_header.scheduled_post_indicator'
>
{scheduledPostText} {scheduledPostText}
{' '} {' '}
<Text <Text

View file

@ -39,7 +39,7 @@ module.exports = {
}, },
}], }],
['module:react-native-dotenv', { ['module:react-native-dotenv', {
moduleName: 'react-native-dotenv', moduleName: '@env',
path: '.env', path: '.env',
blacklist: null, blacklist: null,
whitelist: null, whitelist: null,

View file

@ -60,11 +60,35 @@
} }
}, },
"artifacts": { "artifacts": {
"pathBuilder": "./e2e/path_builder.js" "pathBuilder": "./e2e/path_builder.js",
"plugins": {
"screenshot": {
"shouldTakeAutomaticSnapshots": true,
"keepOnlyFailedTestsArtifacts": true
},
"video": {
"enabled": false
},
"log": {
"enabled": true
}
}
}, },
"behavior": { "behavior": {
"init": { "init": {
"reinstallApp": true,
"launchApp": false "launchApp": false
},
"cleanup": {
"shutdownDevice": false
} }
},
"session": {
"sessionId": "mobile-test-session",
"debugSynchronization": 5000,
"autoStart": true
},
"visibility": {
"threshold": 75
} }
} }

View file

@ -19,15 +19,33 @@ navigate to the `detox` folder and run `npm install`
To build the Detox Android app, navigate to the `detox` folder and run: To build the Detox Android app, navigate to the `detox` folder and run:
```sh ```sh
npm run e2e:android-inject-settings
npm run e2e:android-build npm run e2e:android-build
``` ```
The debug apk will be built and available at `android/app/build/outputs/apk/debug/app-debug.apk`
### Run Detox Android Tests ### Run Detox Android Tests
#### Create emulator
```sh
./create_android_emulator.sh SDK_VERSION AVD_NAME
# example ./create_android_emulator.sh 34 pixel_5a_avd
# example ./create_android_emulator.sh 34 pixel_5a_avd --headless
# If we want to see the emulator logs. Run it in debug mode example ./create_android_emulator.sh 34 pixel_5a_avd --debug
```
To execute the Detox tests on Android, navigate to the `detox` folder and run: To execute the Detox tests on Android, navigate to the `detox` folder and run:
```sh ```sh
npm run e2e:android-test npm run e2e:android-test
# To run a particular tests
npm run e2e:android-test <path to test file>
``` ```
## iOS ## iOS
@ -40,10 +58,30 @@ To build the iOS simulator for Detox, navigate to the `detox` folder and run:
npm run e2e:ios-build npm run e2e:ios-build
``` ```
This will build the Simulor .zip file at the root folder.
Create a folder named `mobile-artifacts` at the project root. Unzip the zip file and move the mattermost app under `mobile-artifacts.`
```sh
# From project root
mkdir mobile-artifacts
```
### Run iOS Tests ### Run iOS Tests
To execute the Detox tests on iOS, navigate to the `detox` folder and run: To execute the Detox tests on iOS, navigate to the `detox` folder and run:
```sh ```sh
npm run e2e:ios-test npm run e2e:ios-test
```
# To run a particular tests
npm run e2e:android-test path to test file.
```
#### TIP : For iOS, you can download the simulator from `~Mobile: Test build` or `~Release: Mobile Apps` channel in the community.
### Results
The Local Runs generate artifacts under `detox/artifacts/ios-debug-**` or `detox/artifacts/android-debug-**`.
You can see the html report, failure screenshot under that folder.

View file

@ -28,7 +28,7 @@ create_avd() {
local cpu_arch_family cpu_arch local cpu_arch_family cpu_arch
read cpu_arch_family cpu_arch < <(get_cpu_architecture) read cpu_arch_family cpu_arch < <(get_cpu_architecture)
avdmanager create avd -n "$AVD_NAME" -k "system-images;android-${SDK_VERSION};default;${cpu_arch_family}" -p "$AVD_NAME" -d 'pixel' avdmanager create avd -n "$AVD_NAME" -k "system-images;android-${SDK_VERSION};google_apis;${cpu_arch_family}" -p "$AVD_NAME" -d 'pixel_5'
cp -r android_emulator/ "$AVD_NAME/" cp -r android_emulator/ "$AVD_NAME/"
sed -i -e "s|AvdId = change_avd_id|AvdId = ${AVD_NAME}|g" "$AVD_NAME/config.ini" sed -i -e "s|AvdId = change_avd_id|AvdId = ${AVD_NAME}|g" "$AVD_NAME/config.ini"
@ -50,7 +50,7 @@ start_adb_server() {
start_emulator() { start_emulator() {
echo "Starting the emulator..." echo "Starting the emulator..."
local emulator_opts="-avd $AVD_NAME -no-snapshot -no-boot-anim -no-audio -no-window" local emulator_opts="-avd $AVD_NAME -no-snapshot -no-boot-anim -no-audio -gpu off -no-window"
if [[ "$CI" == "true" || "$(uname -s)" == "Linux" ]]; then if [[ "$CI" == "true" || "$(uname -s)" == "Linux" ]]; then
emulator $emulator_opts -gpu host -accel on -qemu -m 4096 & emulator $emulator_opts -gpu host -accel on -qemu -m 4096 &
@ -82,6 +82,7 @@ start_server() {
cd .. cd ..
RUNNING_E2E=true npm run start & RUNNING_E2E=true npm run start &
local timeout=120 interval=5 elapsed=0 local timeout=120 interval=5 elapsed=0
sleep $timeout
until nc -z localhost 8081; do until nc -z localhost 8081; do
if [[ $elapsed -ge $timeout ]]; then if [[ $elapsed -ge $timeout ]]; then

View file

@ -8,7 +8,7 @@ module.exports = {
setupFilesAfterEnv: ['./test/setup.ts'], setupFilesAfterEnv: ['./test/setup.ts'],
maxWorkers: 1, maxWorkers: 1,
testSequencer: './custom_sequencer.js', testSequencer: './custom_sequencer.js',
testTimeout: 120000, testTimeout: 180000,
rootDir: '.', rootDir: '.',
testMatch: ['<rootDir>/test/**/*.e2e.ts'], testMatch: ['<rootDir>/test/**/*.e2e.ts'],
transform: { transform: {

View file

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {timeouts, wait} from '@support/utils';
import client from './client'; import client from './client';
import {getResponseFromError} from './common'; import {getResponseFromError} from './common';
@ -72,6 +74,7 @@ export const apiGetPostsInChannel = async (baseUrl: string, channelId: string):
* @return {Object} returns {post} on success or {error, status} on error * @return {Object} returns {post} on success or {error, status} on error
*/ */
export const apiGetLastPostInChannel = async (baseUrl: string, channelId: string): Promise<any> => { export const apiGetLastPostInChannel = async (baseUrl: string, channelId: string): Promise<any> => {
await wait(timeouts.TWO_SEC);
const {posts} = await apiGetPostsInChannel(baseUrl, channelId); const {posts} = await apiGetPostsInChannel(baseUrl, channelId);
return {post: posts[0]}; return {post: posts[0]};
}; };

View file

@ -41,6 +41,7 @@ class Alert {
cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(1); cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).atIndex(1);
confirmButton = isAndroid() ? element(by.text('CONFIRM')) : element(by.label('Confirm')).atIndex(1); confirmButton = isAndroid() ? element(by.text('CONFIRM')) : element(by.label('Confirm')).atIndex(1);
deleteButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(0); 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); leaveButton = isAndroid() ? element(by.text('LEAVE')) : element(by.label('Leave')).atIndex(0);
logoutButton = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(1); logoutButton = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(1);
logoutButton2 = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(2); logoutButton2 = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(2);

View file

@ -19,8 +19,10 @@ class BrowseChannelsScreen {
channelDropdownTextArchived: 'browse_channels.channel_dropdown.text.archived', channelDropdownTextArchived: 'browse_channels.channel_dropdown.text.archived',
channelDropdownTextShared: 'browse_channels.channel_dropdown.text.shared', channelDropdownTextShared: 'browse_channels.channel_dropdown.text.shared',
flatChannelList: 'browse_channels.channel_list.flat_list', flatChannelList: 'browse_channels.channel_list.flat_list',
scheduledPostTooltipCloseButton: 'scheduled_post.tooltip.close.button',
}; };
scheduledPostTooltipCloseButton = element(by.id(this.testID.scheduledPostTooltipCloseButton));
browseChannelsScreen = element(by.id(this.testID.browseChannelsScreen)); browseChannelsScreen = element(by.id(this.testID.browseChannelsScreen));
closeButton = element(by.id(this.testID.closeButton)); closeButton = element(by.id(this.testID.closeButton));
createButton = element(by.id(this.testID.createButton)); createButton = element(by.id(this.testID.createButton));
@ -60,6 +62,15 @@ class BrowseChannelsScreen {
await this.closeButton.tap(); await this.closeButton.tap();
await expect(this.browseChannelsScreen).not.toBeVisible(); await expect(this.browseChannelsScreen).not.toBeVisible();
}; };
dismissScheduledPostTooltip = async () => {
try {
await this.scheduledPostTooltipCloseButton.tap();
} catch (error) {
// eslint-disable-next-line no-console
console.log('Element not visible, skipping click');
}
};
} }
const browseChannelsScreen = new BrowseChannelsScreen(); const browseChannelsScreen = new BrowseChannelsScreen();

View file

@ -17,7 +17,7 @@ import {
PostOptionsScreen, PostOptionsScreen,
ThreadScreen, ThreadScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {timeouts, wait} from '@support/utils'; import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
class ChannelScreen { class ChannelScreen {
@ -47,8 +47,31 @@ class ChannelScreen {
postPriorityUrgentMessage: 'post_priority_picker_item.urgent', postPriorityUrgentMessage: 'post_priority_picker_item.urgent',
postPriorityRequestAck: 'post_priority_picker_item.requested_ack.toggled.false.button', postPriorityRequestAck: 'post_priority_picker_item.requested_ack.toggled.false.button',
postPriorityPersistentNotification: 'post_priority_picker_item.persistent_notifications.toggled.undefined.button', postPriorityPersistentNotification: 'post_priority_picker_item.persistent_notifications.toggled.undefined.button',
scheduledPostTooltipCloseButton: 'scheduled_post.tooltip.close.button',
scheduledPostTooltipCloseButtonAdminAccount: 'scheduled_post_tutorial_tooltip.close',
scheduleMessageTomorrowOption: 'post_priority_picker_item.scheduledPostOptionTomorrow',
scheduleMessageOnMondayOption: 'post_priority_picker_item.scheduledPostOptionMonday',
scheduledPostOptionNextMonday: 'post_priority_picker_item.scheduledPostOptionNextMonday',
scheduledPostOptionTomorrowSelected: 'post_priority_picker_item.scheduledPostOptionTomorrow.selected',
scheduledPostOptionMondaySelected: 'post_priority_picker_item.scheduledPostOptionMonday.selected',
scheduledPostOptionNextMondaySelected: 'post_priority_picker_item.scheduledPostOptionNextMonday.selected',
clickOnScheduledMessageButton: 'scheduled_post_create_button',
scheduledDraftInfoInChannel: 'scheduled_post_header.scheduled_post_indicator',
scheduledDraftTooltipText: 'scheduled_post.tooltip.description',
}; };
scheduleDraftInforMessage = element(by.text('Type a message and long press the send button to schedule it for a later time.'));
scheduledDraftTooltipText = element(by.id(this.testID.scheduledDraftTooltipText));
scheduledDraftInfoInChannel = element(by.id(this.testID.scheduledDraftInfoInChannel));
clickOnScheduledMessageButton = element(by.id(this.testID.clickOnScheduledMessageButton));
scheduledPostOptionTomorrowSelected = element(by.id(this.testID.scheduledPostOptionTomorrowSelected));
scheduledPostOptionMondaySelected = element(by.id(this.testID.scheduledPostOptionMondaySelected));
scheduledPostOptionNextMonday = element(by.id(this.testID.scheduledPostOptionNextMonday));
scheduledPostOptionNextMondaySelected = element(by.id(this.testID.scheduledPostOptionNextMondaySelected));
scheduleMessageTomorrowOption = element(by.id(this.testID.scheduleMessageTomorrowOption));
scheduleMessageOnMondayOption = element(by.id(this.testID.scheduleMessageOnMondayOption));
scheduledPostTooltipCloseButton = element(by.id(this.testID.scheduledPostTooltipCloseButton));
scheduledPostTooltipCloseButtonAdminAccount = element(by.id(this.testID.scheduledPostTooltipCloseButtonAdminAccount));
postPriorityPersistentNotification = element(by.id(this.testID.postPriorityPersistentNotification)); postPriorityPersistentNotification = element(by.id(this.testID.postPriorityPersistentNotification));
postPriorityUrgentMessage = element(by.id(this.testID.postPriorityUrgentMessage)); postPriorityUrgentMessage = element(by.id(this.testID.postPriorityUrgentMessage));
postPriorityRequestAck = element(by.id(this.testID.postPriorityRequestAck)); postPriorityRequestAck = element(by.id(this.testID.postPriorityRequestAck));
@ -132,7 +155,12 @@ class ChannelScreen {
open = async (categoryKey: string, channelName: string) => { open = async (categoryKey: string, channelName: string) => {
// # Open channel screen // # Open channel screen
await ChannelListScreen.getChannelItemDisplayName(categoryKey, channelName).tap(); await ChannelListScreen.getChannelItemDisplayName(categoryKey, channelName).tap();
try {
await this.scheduledPostTooltipCloseButton.tap();
} catch (error) {
// eslint-disable-next-line no-console
console.log('Element not visible, skipping click');
}
return this.toBeVisible(); return this.toBeVisible();
}; };
@ -169,7 +197,7 @@ class ChannelScreen {
await expect(postListPostItem).toBeVisible(); await expect(postListPostItem).toBeVisible();
// # Open post options // # Open post options
await postListPostItem.longPress(); await postListPostItem.longPress(timeouts.TWO_SEC);
await PostOptionsScreen.toBeVisible(); await PostOptionsScreen.toBeVisible();
await wait(timeouts.TWO_SEC); await wait(timeouts.TWO_SEC);
}; };
@ -190,6 +218,12 @@ class ChannelScreen {
await this.tapSendButton(); await this.tapSendButton();
}; };
enterMessageToSchedule = async (message: string) => {
await this.postInput.tap();
await this.postInput.clearText();
await this.postInput.replaceText(message);
};
tapSendButton = async () => { tapSendButton = async () => {
// # Tap send button // # Tap send button
await this.sendButton.tap(); await this.sendButton.tap();
@ -197,6 +231,11 @@ class ChannelScreen {
await expect(this.sendButtonDisabled).toBeVisible(); await expect(this.sendButtonDisabled).toBeVisible();
}; };
longPressSendButton = async () => {
// # Long press send button
await this.sendButton.longPress();
};
hasPostMessage = async (postId: string, postMessage: string) => { hasPostMessage = async (postId: string, postMessage: string) => {
const {postListPostItem} = this.getPostListPostItem(postId, postMessage); const {postListPostItem} = this.getPostListPostItem(postId, postMessage);
await expect(postListPostItem).toBeVisible(); await expect(postListPostItem).toBeVisible();
@ -231,6 +270,47 @@ class ChannelScreen {
applyPostPrioritySettings = async () => { applyPostPrioritySettings = async () => {
await this.applyPostPriority.tap(); await this.applyPostPriority.tap();
}; };
scheduleMessageForTomorrow = async () => {
await this.scheduleMessageTomorrowOption.tap();
await expect(this.scheduledPostOptionTomorrowSelected).toBeVisible();
};
scheduleMessageForMonday = async () => {
await this.scheduleMessageOnMondayOption.tap();
await expect(this.scheduledPostOptionMondaySelected).toBeVisible();
};
scheduleMessageForNextMonday = async () => {
await this.scheduledPostOptionNextMonday.tap();
await expect(this.scheduledPostOptionNextMondaySelected).toBeVisible();
};
clickOnScheduledMessage = async () => {
await this.clickOnScheduledMessageButton.tap();
await waitFor(this.clickOnScheduledMessageButton).not.toBeVisible().withTimeout(timeouts.FOUR_SEC);
};
/*
* Verify the message is scheduled and user can see Info in channel or thread
* @param {boolean} thread - true if the message is scheduled in thread
* @returns {Promise<void>}
*/
verifyScheduledDraftInfoInChannel = async (thread = false) => {
await expect(this.scheduledDraftInfoInChannel).toBeVisible();
await expect(this.scheduledDraftInfoInChannel).toHaveText(`1 scheduled message in ${thread ? 'thread' : 'channel'}. See all.`);
};
closeScheduledMessageTooltip = async () => {
if (isIos()) {
await waitFor(this.scheduledDraftTooltipText).toBeVisible().withTimeout(timeouts.TEN_SEC);
await this.scheduledDraftTooltipText.tap();
} else {
// The page re-renders and then opens the tooltip again. Wait for the tooltip to be stable and then tap it.
await wait(timeouts.TEN_SEC);
await this.scheduleDraftInforMessage.tap();
}
};
} }
const channelScreen = new ChannelScreen(); const channelScreen = new ChannelScreen();

View file

@ -116,6 +116,7 @@ class ChannelInfoScreen {
noButton, noButton,
yesButton, yesButton,
} = Alert; } = Alert;
await wait(timeouts.TWO_SEC);
await expect(alertArchiveChannelTitle).toBeVisible(); await expect(alertArchiveChannelTitle).toBeVisible();
await expect(noButton).toBeVisible(); await expect(noButton).toBeVisible();
await expect(yesButton).toBeVisible(); await expect(yesButton).toBeVisible();
@ -141,7 +142,7 @@ class ChannelInfoScreen {
convertToPrivateChannel = async (channelDisplayName: string, {confirm = true} = {}) => { convertToPrivateChannel = async (channelDisplayName: string, {confirm = true} = {}) => {
await this.scrollView.tap({x: 1, y: 1}); await this.scrollView.tap({x: 1, y: 1});
await this.scrollView.scroll(100, 'down'); await this.scrollView.scroll(100, 'down');
await waitFor(this.convertPrivateOption).toExist().withTimeout(timeouts.TWO_SEC); await waitFor(this.convertPrivateOption).toBeVisible().whileElement(by.id(this.testID.scrollView)).scroll(50, 'down');
await this.convertPrivateOption.tap({x: 1, y: 1}); await this.convertPrivateOption.tap({x: 1, y: 1});
const { const {
channelNowPrivateTitle, channelNowPrivateTitle,
@ -205,6 +206,7 @@ class ChannelInfoScreen {
unarchiveChannel = async (alertUnarchiveChannelTitle: Detox.NativeElement, {confirm = true} = {}) => { unarchiveChannel = async (alertUnarchiveChannelTitle: Detox.NativeElement, {confirm = true} = {}) => {
await waitFor(this.unarchiveChannelOption).toBeVisible().whileElement(by.id(this.testID.scrollView)).scroll(50, 'down'); await waitFor(this.unarchiveChannelOption).toBeVisible().whileElement(by.id(this.testID.scrollView)).scroll(50, 'down');
await wait(timeouts.TWO_SEC);
await this.unarchiveChannelOption.tap({x: 1, y: 1}); await this.unarchiveChannelOption.tap({x: 1, y: 1});
const { const {
noButton, noButton,

View file

@ -15,6 +15,7 @@ class ChannelListScreen {
draftChannelInfo: 'draft_post.channel_info', draftChannelInfo: 'draft_post.channel_info',
draftbuttonListScreen: 'channel_list.drafts.button', draftbuttonListScreen: 'channel_list.drafts.button',
draftCountListScreen: 'channel_list.drafts.count', draftCountListScreen: 'channel_list.drafts.count',
scheduledMessageCountListScreen: 'channel_list.scheduled_post.count',
teamItemPrefix: 'team_sidebar.team_list.team_item.', teamItemPrefix: 'team_sidebar.team_list.team_item.',
channelListScreen: 'channel_list.screen', channelListScreen: 'channel_list.screen',
serverIcon: 'channel_list.servers.server_icon', serverIcon: 'channel_list.servers.server_icon',

View file

@ -8,6 +8,7 @@ import {expect} from 'detox';
class CreateDirectMessageScreen { class CreateDirectMessageScreen {
testID = { testID = {
selectedDMUserPrefix: 'create_direct_message.',
selectedUserPrefix: 'create_direct_message.selected_user.', selectedUserPrefix: 'create_direct_message.selected_user.',
userItemPrefix: 'create_direct_message.user_list.user_item.', userItemPrefix: 'create_direct_message.user_list.user_item.',
createDirectMessageScreen: 'create_direct_message.screen', createDirectMessageScreen: 'create_direct_message.screen',
@ -20,8 +21,10 @@ class CreateDirectMessageScreen {
sectionUserList: 'create_direct_message.user_list.section_list', sectionUserList: 'create_direct_message.user_list.section_list',
tutorialHighlight: 'tutorial_highlight', tutorialHighlight: 'tutorial_highlight',
tutorialSwipeLeft: 'tutorial_swipe_left', tutorialSwipeLeft: 'tutorial_swipe_left',
scheduledPostTooltipCloseButton: 'scheduled_post.tooltip.close.button',
}; };
scheduledPostTooltipCloseButton = element(by.id(this.testID.scheduledPostTooltipCloseButton));
createDirectMessageScreen = element(by.id(this.testID.createDirectMessageScreen)); createDirectMessageScreen = element(by.id(this.testID.createDirectMessageScreen));
closeButton = element(by.id(this.testID.closeButton)); closeButton = element(by.id(this.testID.closeButton));
startButton = element(by.id(this.testID.startButton)); startButton = element(by.id(this.testID.startButton));
@ -41,6 +44,10 @@ class CreateDirectMessageScreen {
return element(by.id(`${this.testID.selectedUserPrefix}${userId}.display_name`)); return element(by.id(`${this.testID.selectedUserPrefix}${userId}.display_name`));
}; };
getSelectedDMUserDisplayName = (userId: string) => {
return element(by.id(`${this.testID.selectedDMUserPrefix}${userId}.display_name`));
};
getSelectedUserRemoveButton = (userId: string) => { getSelectedUserRemoveButton = (userId: string) => {
return element(by.id(`${this.testID.selectedUserPrefix}${userId}.remove.button`)); return element(by.id(`${this.testID.selectedUserPrefix}${userId}.remove.button`));
}; };
@ -70,7 +77,7 @@ class CreateDirectMessageScreen {
await ChannelListScreen.headerPlusButton.tap(); await ChannelListScreen.headerPlusButton.tap();
await wait(timeouts.ONE_SEC); await wait(timeouts.ONE_SEC);
await ChannelListScreen.openDirectMessageItem.tap(); await ChannelListScreen.openDirectMessageItem.tap();
await wait(timeouts.TEN_SEC);
return this.toBeVisible(); return this.toBeVisible();
}; };
@ -80,14 +87,19 @@ class CreateDirectMessageScreen {
}; };
closeTutorial = async () => { closeTutorial = async () => {
if (isIos()) { try {
await waitFor(this.tutorialHighlight).toExist().withTimeout(timeouts.TEN_SEC); if (isIos()) {
await this.tutorialSwipeLeft.tap(); await waitFor(this.tutorialHighlight).toExist().withTimeout(timeouts.HALF_MIN);
await expect(this.tutorialHighlight).not.toExist(); await this.tutorialSwipeLeft.tap();
} else { await expect(this.tutorialHighlight).not.toExist();
await wait(timeouts.ONE_SEC); } else {
await device.pressBack(); await wait(timeouts.ONE_SEC);
await wait(timeouts.ONE_SEC); await device.pressBack();
await wait(timeouts.ONE_SEC);
}
} catch {
// eslint-disable-next-line no-console
console.log('Tutorial element not visible, skipping action:');
} }
}; };
} }

View file

@ -60,7 +60,7 @@ class CreateOrEditChannelScreen {
openEditChannel = async () => { openEditChannel = async () => {
// # Open edit channel screen // # Open edit channel screen
await ChannelInfoScreen.scrollView.tap({x: 1, y: 1}); await ChannelInfoScreen.scrollView.tap({x: 1, y: 1});
await ChannelInfoScreen.scrollView.scroll(100, 'down'); await ChannelInfoScreen.scrollView.scrollTo('bottom');
await ChannelInfoScreen.editChannelOption.tap(); await ChannelInfoScreen.editChannelOption.tap();
return this.toBeVisible(); return this.toBeVisible();
@ -96,6 +96,16 @@ class CreateOrEditChannelScreen {
await this.makePrivateToggledOn.tap(); await this.makePrivateToggledOn.tap();
await expect(this.makePrivateToggledOff).toBeVisible(); await expect(this.makePrivateToggledOff).toBeVisible();
}; };
clickonCreateButton = async () => {
await this.createButton.tap();
try {
await ChannelScreen.scheduledPostTooltipCloseButton.tap();
} catch (error) {
// eslint-disable-next-line no-console
console.log('Element not visible, skipping click');
}
};
} }
const createOrEditChannelScreen = new CreateOrEditChannelScreen(); const createOrEditChannelScreen = new CreateOrEditChannelScreen();

View file

@ -12,7 +12,7 @@ class DraftScreen {
draftMessageContent: 'draft_message', draftMessageContent: 'draft_message',
draftScreen: 'global_drafts_list', draftScreen: 'global_drafts_list',
draftTooltipCloseButton: 'draft.tooltip.close.button', draftTooltipCloseButton: 'draft.tooltip.close.button',
draftPost: 'draft_post', draftPost: 'draft_message',
draftSendButton: 'send_draft_button', draftSendButton: 'send_draft_button',
draftEmptyTitle: 'drafts.empty.title', draftEmptyTitle: 'drafts.empty.title',
requestACKIcon: 'drafts.requested_ack.icon', requestACKIcon: 'drafts.requested_ack.icon',

View file

@ -13,10 +13,10 @@ class EditProfileScreen {
closeButton: 'close.edit_profile.button', closeButton: 'close.edit_profile.button',
saveButton: 'edit_profile.save.button', saveButton: 'edit_profile.save.button',
scrollView: 'edit_profile.scroll_view', scrollView: 'edit_profile.scroll_view',
firstNameInput: 'edit_profile_form.first_name.input', firstNameInput: 'edit_profile_form.firstName.input',
firstNameInputDisabled: 'edit_profile_form.first_name.input.disabled', firstNameInputDisabled: 'edit_profile_form.firstName.input.disabled',
lastNameInput: 'edit_profile_form.last_name.input', lastNameInput: 'edit_profile_form.lastName.input',
lastNameInputDisabled: 'edit_profile_form.last_name.input.disabled', lastNameInputDisabled: 'edit_profile_form.lastName.input.disabled',
usernameInput: 'edit_profile_form.username.input', usernameInput: 'edit_profile_form.username.input',
usernameInputDisabled: 'edit_profile_form.username.input.disabled', usernameInputDisabled: 'edit_profile_form.username.input.disabled',
emailInput: 'edit_profile_form.email.input', emailInput: 'edit_profile_form.email.input',

View file

@ -35,6 +35,7 @@ import PushNotificationSettingsScreen from './push_notification_settings';
import ReactionsScreen from './reactions'; import ReactionsScreen from './reactions';
import RecentMentionsScreen from './recent_mentions'; import RecentMentionsScreen from './recent_mentions';
import SavedMessagesScreen from './saved_messages'; import SavedMessagesScreen from './saved_messages';
import ScheduleMessageScreen from './scheduled_message_screen';
import SearchMessagesScreen from './search_messages'; import SearchMessagesScreen from './search_messages';
import SelectTimezoneScreen from './select_timezone'; import SelectTimezoneScreen from './select_timezone';
import ServerScreen from './server'; import ServerScreen from './server';
@ -95,4 +96,5 @@ export {
TimezoneDisplaySettingsScreen, TimezoneDisplaySettingsScreen,
UserProfileScreen, UserProfileScreen,
DraftScreen, DraftScreen,
ScheduleMessageScreen,
}; };

View file

@ -79,6 +79,10 @@ class InviteScreen {
return element(by.id(`${this.testID.selectedItemPrefix}.${id}`)); return element(by.id(`${this.testID.selectedItemPrefix}.${id}`));
}; };
getSelectedChip = () => {
return element(by.id('invite.selected_item.display_name'));
};
getSummaryReportSent = () => { getSummaryReportSent = () => {
return element(by.id(`${this.testID.summaryReportPrefix}.sent`)); return element(by.id(`${this.testID.summaryReportPrefix}.sent`));
}; };

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {ChannelListScreen, ServerScreen} from '@support/ui/screen'; import {ChannelListScreen, ServerScreen} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils'; import {isAndroid, retryWithReload, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
class LoginScreen { class LoginScreen {
@ -21,8 +21,10 @@ class LoginScreen {
forgotPasswordButton: 'login_form.forgot_password.button', forgotPasswordButton: 'login_form.forgot_password.button',
signinButton: 'login_form.signin.button', signinButton: 'login_form.signin.button',
signinButtonDisabled: 'login_form.signin.button.disabled', signinButtonDisabled: 'login_form.signin.button.disabled',
loginFormInfoText: 'login_options.description.enter_credentials',
}; };
loginFormInfoText = element(by.id(this.testID.loginFormInfoText));
loginScreen = element(by.id(this.testID.loginScreen)); loginScreen = element(by.id(this.testID.loginScreen));
backButton = element(by.id(this.testID.backButton)); backButton = element(by.id(this.testID.backButton));
titleLoginToAccount = element(by.id(this.testID.titleLoginToAccount)); titleLoginToAccount = element(by.id(this.testID.titleLoginToAccount));
@ -58,17 +60,20 @@ class LoginScreen {
await expect(this.loginScreen).not.toBeVisible(); await expect(this.loginScreen).not.toBeVisible();
}; };
login = async (user: any = {}) => { loginWithRetryIfStuck = async (user: any = {}) => {
await this.toBeVisible(); await this.toBeVisible();
await this.usernameInput.tap({x: 150, y: 10}); await this.usernameInput.tap({x: 150, y: 10});
await this.usernameInput.replaceText(user.newUser.email); await this.usernameInput.replaceText(user.newUser.email);
await this.passwordInput.tap(); await this.passwordInput.tap();
await this.passwordInput.replaceText(user.newUser.password); await this.passwordInput.replaceText(user.newUser.password);
await element(by.text(/^Log In to Your Account*$/)).tap(); await this.loginFormInfoText.tap();
await this.signinButton.tap(); await this.signinButton.tap();
await waitFor(ChannelListScreen.channelListScreen).toExist().withTimeout(timeouts.TEN_SEC); await waitFor(ChannelListScreen.channelListScreen).toBeVisible().withTimeout(isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN);
};
login = async (user: any = {}) => {
await retryWithReload(() => this.loginWithRetryIfStuck(user));
}; };
loginAsAdmin = async (user: any = {}) => { loginAsAdmin = async (user: any = {}) => {
@ -78,9 +83,9 @@ class LoginScreen {
await this.usernameInput.replaceText(user.username); await this.usernameInput.replaceText(user.username);
await this.passwordInput.tap(); await this.passwordInput.tap();
await this.passwordInput.replaceText(user.password); await this.passwordInput.replaceText(user.password);
await this.loginFormInfoText.tap();
await this.signinButton.tap(); await this.signinButton.tap();
await waitFor(ChannelListScreen.channelListScreen).toBeVisible().withTimeout(isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN);
await waitFor(ChannelListScreen.channelListScreen).toExist().withTimeout(timeouts.TEN_SEC);
}; };
} }

View file

@ -0,0 +1,167 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from '@support/ui/component';
import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
class ScheduledMessageScreen {
testID = {
customDateTimePickerScreen: 'custom_date_time_picker',
deleteDraft: 'delete_draft',
rescheduleOption: 'rescheduled_draft',
scheduledTab: 'scheduled_post_tab',
scheduledTabBadgeCount: 'scheduled_post_count_badge',
scheduledMessageTooltipCloseButton: 'draft.tooltip.close.button',
scheduledMessageText: 'markdown_paragraph',
scheduledDraftTime: 'scheduled_post_header.scheduled_at',
};
selectDateButton = element(by.text(isIos()? 'Select Date': 'SELECT DATE'));
selectTimeButton = element(by.text(isIos()? 'Select Time': 'SELECT TIME'));
saveButton = element(by.text(isIos()? 'Save': 'SAVE'));
androidCalenderOkButton = element(by.text('OK'));
scheduledDraftTime = element(by.id(this.testID.scheduledDraftTime));
customDateTimePickerScreen = element(by.id(this.testID.customDateTimePickerScreen));
rescheduleOption = element(by.id(this.testID.rescheduleOption));
deleteDraftSwipeAction = element(by.text('Delete draft'));
scheduledMessageText = element(by.id(this.testID.scheduledMessageText));
deleteDraft = element(by.id(this.testID.deleteDraft));
scheduledMessageTooltipCloseButton = {
tap: async () => {
await element(by.id(this.testID.scheduledMessageTooltipCloseButton)).tap();
},
};
clickScheduledTab = async () => {
await element(by.id(this.testID.scheduledTab)).tap();
try {
await this.scheduledMessageTooltipCloseButton.tap();
} catch (error) {
// eslint-disable-next-line no-console
console.log('Element not visible, skipping click');
}
};
verifyCountOnScheduledTab = async (count: string) => {
await expect(element(by.id(this.testID.scheduledTabBadgeCount))).toHaveText(count);
};
assertScheduledMessageExists = async (scheduledMessageText: string) => {
await waitFor(this.scheduledMessageText).toBeVisible().withTimeout(500);
await expect(element(by.text(scheduledMessageText))).toBeVisible();
};
deleteDraftPost = async (deleteAction: any) => {
await expect(deleteAction).toBeVisible();
await deleteAction.tap();
await waitFor(Alert.deleteScheduledMessageButton).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.deleteScheduledMessageButton.tap();
};
deleteDraftPostFromSwipeActions = async () => {
await this.deleteDraftPost(this.deleteDraftSwipeAction);
};
deleteScheduledMessageFromDraftActions = async () => {
await this.deleteDraftPost(this.deleteDraft);
};
selectDateTime = async () => {
await this.selectDateButton.tap();
await this.selectTimeButton.tap();
await this.saveButton.tap();
};
clickRescheduleOption = async () => {
await this.rescheduleOption.tap();
if (isIos()) {
await waitFor(this.customDateTimePickerScreen).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await expect(this.customDateTimePickerScreen).toBeVisible();
} else {
// to close native calander picker
await device.pressBack();
}
};
/**
* Asserts that the element has the expected text.
* @param expectedText - The text you expect in the element
*/
assertScheduleTimeTextIsVisible = async (expectedText: string) => {
const attr = await this.scheduledDraftTime.getAttributes();
const actualText = 'text' in attr ? attr.text : null;
if (this.normalize(actualText || '') !== this.normalize(expectedText)) {
throw new Error(`Expected text "${expectedText}" but found "${actualText}"`);
}
};
getRoundedTime = async (): Promise<Date> => {
const now = new Date();
const minutes = now.getMinutes();
if (minutes === 0 || minutes === 30) {
// Waiting 60 seconds to avoid edge case at HH:00 or HH:30...
await wait(timeouts.ONE_MIN);
return this.getRoundedTime(); // Retry after wait
}
if (minutes < 30) {
now.setMinutes(30, 0, 0);
} else {
now.setHours(now.getHours() + 1, 0, 0, 0);
}
return now;
};
nextMonday = async () => {
const today = new Date();
const dayOfWeek = today.getDay();
const daysUntilNextMonday = (8 - dayOfWeek) % 7 || 7;
const nextMonday = new Date(today);
nextMonday.setDate(today.getDate() + daysUntilNextMonday);
nextMonday.setHours(9, 0, 0, 0); // Hardcoded 9:00 AM
const locale = 'en-US';
const dateOptions: Intl.DateTimeFormatOptions = {month: 'short', day: 'numeric'};
const timeOptions: Intl.DateTimeFormatOptions = {hour: 'numeric', minute: '2-digit', hour12: true};
const datePart = nextMonday.toLocaleDateString(locale, dateOptions);
const timePart = nextMonday.toLocaleTimeString(locale, timeOptions);
return this.normalize(`Send on ${datePart}, ${timePart}`);
};
currentDay = async () => {
const adjustedTime = await this.getRoundedTime();
const locale = 'en-US';
const dateOptions: Intl.DateTimeFormatOptions = {month: 'short', day: 'numeric'};
const timeOptions: Intl.DateTimeFormatOptions = {
hour: 'numeric',
minute: '2-digit',
hour12: true,
};
const datePart = adjustedTime.toLocaleDateString(locale, dateOptions);
const timePart = adjustedTime.toLocaleTimeString(locale, timeOptions);
return this.normalize(`Send on ${datePart}, ${timePart}`);
};
/**
* Normalizes text by trimming, collapsing spaces, and replacing narrow no-break spaces.
*/
normalize = (s: string) =>
s.replace(/\u202F/g, ' '). // replace narrow no-break spaces
replace(/\s+/g, ' '). // collapse multiple spaces
trim();
}
const scheduledMessageScreen = new ScheduledMessageScreen();
export default scheduledMessageScreen;

View file

@ -68,7 +68,7 @@ class SearchMessagesScreen {
}; };
getRecentSearchItemRemoveButton = (searchTerm: string) => { getRecentSearchItemRemoveButton = (searchTerm: string) => {
return element(by.id(`search.recent_item.${searchTerm}.remove.button`)); return element(by.id(`search.recent_item.${searchTerm}.remove.button`)).atIndex(0);
}; };
toBeVisible = async () => { toBeVisible = async () => {
@ -87,6 +87,7 @@ class SearchMessagesScreen {
openPostOptionsFor = async (postId: string, text: string) => { openPostOptionsFor = async (postId: string, text: string) => {
const {postListPostItem} = this.getPostListPostItem(postId, text); const {postListPostItem} = this.getPostListPostItem(postId, text);
await wait(timeouts.TWO_SEC);
await expect(postListPostItem).toBeVisible(); await expect(postListPostItem).toBeVisible();
// # Open post options // # Open post options

View file

@ -66,7 +66,7 @@ class ServerScreen {
} }
} }
} }
await waitFor(this.usernameInput).toExist().withTimeout(timeouts.TEN_SEC); await waitFor(this.usernameInput).toExist().withTimeout(isAndroid()? timeouts.ONE_MIN : timeouts.HALF_MIN);
}; };
close = async () => { close = async () => {

View file

@ -92,7 +92,7 @@ class ThreadScreen {
back = async () => { back = async () => {
await this.backButton.tap(); await this.backButton.tap();
await expect(this.threadScreen).not.toBeVisible(); await waitFor(this.threadScreen).not.toBeVisible().withTimeout(timeouts.TEN_SEC);
}; };
openPostOptionsFor = async (postId: string, text: string) => { openPostOptionsFor = async (postId: string, text: string) => {
@ -112,6 +112,17 @@ class ThreadScreen {
await this.tapSendButton(); await this.tapSendButton();
}; };
enterMessageToSchedule = async (message: string) => {
await this.postInput.tap();
await this.postInput.clearText();
await this.postInput.replaceText(message);
};
longPressSendButton = async () => {
// # Long press send button
await this.sendButton.longPress();
};
tapSendButton = async () => { tapSendButton = async () => {
// # Tap send button // # Tap send button
await this.sendButton.tap(); await this.sendButton.tap();

View file

@ -77,3 +77,28 @@ export const timeouts = {
FOUR_MIN: MINUTE * 4, FOUR_MIN: MINUTE * 4,
}; };
/**
* Retry a function with reload
* @param {function} func - function to retry
* @param {number} retries - number of retries
* @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> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
// eslint-disable-next-line no-await-in-loop
await func();
return;
} catch (err) {
if (attempt < retries) {
// 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));
} else {
throw err;
}
}
}
}

View file

@ -54,6 +54,7 @@ describe('Account - Edit Profile', () => {
await expect(EditProfileScreen.closeButton).toBeVisible(); await expect(EditProfileScreen.closeButton).toBeVisible();
await expect(EditProfileScreen.saveButton).toBeVisible(); await expect(EditProfileScreen.saveButton).toBeVisible();
await expect(EditProfileScreen.getEditProfilePicture(testUser.id)).toBeVisible(); await expect(EditProfileScreen.getEditProfilePicture(testUser.id)).toBeVisible();
await waitFor(EditProfileScreen.firstNameInput).toExist().withTimeout(2000);
if (isIos()) { if (isIos()) {
await expect(EditProfileScreen.firstNameInput).toHaveValue(testUser.first_name); await expect(EditProfileScreen.firstNameInput).toHaveValue(testUser.first_name);
await expect(EditProfileScreen.lastNameInput).toHaveValue(testUser.last_name); await expect(EditProfileScreen.lastNameInput).toHaveValue(testUser.last_name);

View file

@ -21,7 +21,7 @@ import {
ServerScreen, ServerScreen,
SettingsScreen, SettingsScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getRandomId, isIos} from '@support/utils'; import {getRandomId, isIos, timeouts} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Account - Settings - Mention Notification Settings', () => { describe('Account - Settings - Mention Notification Settings', () => {
@ -84,7 +84,7 @@ describe('Account - Settings - Mention Notification Settings', () => {
const triggerMentionKeyword = await MentionNotificationSettingsScreen.getKeywordTriggerElement(keywords); const triggerMentionKeyword = await MentionNotificationSettingsScreen.getKeywordTriggerElement(keywords);
await expect(triggerMentionKeyword).toHaveText(keywords.replace(/ /g, '').toLowerCase()); await expect(triggerMentionKeyword).toHaveText(keywords.replace(/ /g, '').toLowerCase());
} else { } else {
await expect(MentionNotificationSettingsScreen.keywordsInput).toHaveText(keywords.replace(/ /g, '').toLowerCase()); await waitFor(element(by.text(keywords.replace(/ /g, '').toLowerCase()))).toExist().withTimeout(timeouts.TWO_SEC);
} }
// # Switch toggles back to original state, clear keywords, tap on back button, and go back to mention notifications screen // # Switch toggles back to original state, clear keywords, tap on back button, and go back to mention notifications screen

View file

@ -66,7 +66,6 @@ describe('Account - Settings - Theme Display Settings', () => {
it('MM-T5111_2 - should be able to select a theme and save by tapping navigation back button', async () => { it('MM-T5111_2 - should be able to select a theme and save by tapping navigation back button', async () => {
// # Tap on a sapphire option and tap on back button // # Tap on a sapphire option and tap on back button
await ThemeDisplaySettingsScreen.sapphireOption.tap(); await ThemeDisplaySettingsScreen.sapphireOption.tap();
await ThemeDisplaySettingsScreen.back();
// * Verify on display settings screen and sapphire is set // * Verify on display settings screen and sapphire is set
await DisplaySettingsScreen.toBeVisible(); await DisplaySettingsScreen.toBeVisible();
@ -80,7 +79,6 @@ describe('Account - Settings - Theme Display Settings', () => {
// # Tap on denim option and tap on back button // # Tap on denim option and tap on back button
await ThemeDisplaySettingsScreen.denimOption.tap(); await ThemeDisplaySettingsScreen.denimOption.tap();
await ThemeDisplaySettingsScreen.back();
// * Verify on display settings screen and denim is set // * Verify on display settings screen and denim is set
await DisplaySettingsScreen.toBeVisible(); await DisplaySettingsScreen.toBeVisible();

View file

@ -52,7 +52,7 @@ describe('Autocomplete - At-Mention', () => {
// # Log in to server // # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser); await LoginScreen.login(user);
// * Verify on channel list screen // * Verify on channel list screen
await ChannelListScreen.toBeVisible(); await ChannelListScreen.toBeVisible();

View file

@ -154,7 +154,7 @@ describe('Autocomplete - Slash Suggestion', () => {
await slashSuggestionAutocomplete.tap(); await slashSuggestionAutocomplete.tap();
// * Verify slash suggestion list disappears // * Verify slash suggestion list disappears
await expect(Autocomplete.flatSlashSuggestionList).not.toBeVisible(); await waitFor(Autocomplete.flatSlashSuggestionList).not.toExist().withTimeout(timeouts.FOUR_SEC);
// # Type in "/" again to re-activate slash suggestion list // # Type in "/" again to re-activate slash suggestion list
await ChannelScreen.postInput.typeText('/'); await ChannelScreen.postInput.typeText('/');

View file

@ -85,6 +85,7 @@ describe('Channels - Browse Channels', () => {
// # Tap on the new public channel item // # Tap on the new public channel item
await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2); await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2);
await BrowseChannelsScreen.dismissScheduledPostTooltip();
// * Verify on newly joined public channel screen // * Verify on newly joined public channel screen
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();

View file

@ -61,6 +61,7 @@ describe('Channels - Channel Info', () => {
await expect(ChannelInfoScreen.muteAction).toBeVisible(); await expect(ChannelInfoScreen.muteAction).toBeVisible();
await expect(ChannelInfoScreen.joinStartCallAction).toBeVisible(); await expect(ChannelInfoScreen.joinStartCallAction).toBeVisible();
await expect(ChannelInfoScreen.ignoreMentionsOptionToggledOff).toBeVisible(); await expect(ChannelInfoScreen.ignoreMentionsOptionToggledOff).toBeVisible();
await ChannelInfoScreen.scrollView.scrollTo('bottom');
await expect(ChannelInfoScreen.pinnedMessagesOption).toBeVisible(); await expect(ChannelInfoScreen.pinnedMessagesOption).toBeVisible();
await expect(ChannelInfoScreen.copyChannelLinkOption).toBeVisible(); await expect(ChannelInfoScreen.copyChannelLinkOption).toBeVisible();
await ChannelInfoScreen.scrollView.scrollTo('bottom'); await ChannelInfoScreen.scrollView.scrollTo('bottom');

View file

@ -82,6 +82,7 @@ describe('Channels - Channel List', () => {
it('MM-T4728_2 - should be able to switch between channels', async () => { it('MM-T4728_2 - should be able to switch between channels', async () => {
// # Tap on a first channel // # Tap on a first channel
await ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name).tap(); await ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name).tap();
await ChannelScreen.closeScheduledMessageTooltip();
// * Verify on first channel // * Verify on first channel
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();

View file

@ -17,16 +17,18 @@ import {
ServerScreen, ServerScreen,
ChannelInfoScreen, ChannelInfoScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getAdminAccount, getRandomId, timeouts} from '@support/utils'; import {getAdminAccount, getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Convert to Private Channel', () => { describe('Channels - Convert to Private Channel', () => {
const siteOneDisplayName = 'Server 1'; const siteOneDisplayName = 'Server 1';
beforeAll(async () => { beforeAll(async () => {
// # Log in to server as admin // # Log in to server as admin
await ServerScreen.connectToServer(siteOneUrl, siteOneDisplayName); await ServerScreen.connectToServer(siteOneUrl, siteOneDisplayName);
await LoginScreen.loginAsAdmin(getAdminAccount()); await LoginScreen.loginAsAdmin(getAdminAccount());
await wait(timeouts.TWO_SEC);
}); });
beforeEach(async () => { beforeEach(async () => {
@ -45,6 +47,8 @@ describe('Channels - Convert to Private Channel', () => {
await CreateOrEditChannelScreen.openCreateChannel(); await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName); await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName);
await CreateOrEditChannelScreen.createButton.tap(); await CreateOrEditChannelScreen.createButton.tap();
await wait(timeouts.TWO_SEC);
await ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount.tap();
await ChannelInfoScreen.open(); await ChannelInfoScreen.open();
await ChannelInfoScreen.convertToPrivateChannel(channelDisplayName, {confirm: true}); await ChannelInfoScreen.convertToPrivateChannel(channelDisplayName, {confirm: true});

View file

@ -20,7 +20,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getRandomId, isIos} from '@support/utils'; import {getRandomId, isIos, timeouts} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Create Channel and Edit Channel Header', () => { describe('Channels - Create Channel and Edit Channel Header', () => {
@ -75,6 +75,8 @@ describe('Channels - Create Channel and Edit Channel Header', () => {
await CreateOrEditChannelScreen.purposeInput.replaceText(purpose); await CreateOrEditChannelScreen.purposeInput.replaceText(purpose);
await CreateOrEditChannelScreen.headerInput.replaceText(header); await CreateOrEditChannelScreen.headerInput.replaceText(header);
await CreateOrEditChannelScreen.createButton.tap(); await CreateOrEditChannelScreen.createButton.tap();
await waitFor(ChannelScreen.scheduledPostTooltipCloseButton).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await ChannelScreen.scheduledPostTooltipCloseButton.tap();
// * Verify on newly created public channel // * Verify on newly created public channel
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();

View file

@ -25,6 +25,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Create Direct Message', () => { describe('Channels - Create Direct Message', () => {
@ -87,10 +88,12 @@ describe('Channels - Create Direct Message', () => {
await CreateDirectMessageScreen.getUserItem(newUser.id).tap(); await CreateDirectMessageScreen.getUserItem(newUser.id).tap();
// * Verify the new user is selected // * Verify the new user is selected
await expect(CreateDirectMessageScreen.getSelectedUserDisplayName(newUser.id)).toBeVisible(); await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(newUser.id)).toBeVisible();
// # Tap on start button // # Tap on start button
await CreateDirectMessageScreen.startButton.tap(); await CreateDirectMessageScreen.startButton.tap();
await waitFor(ChannelScreen.scheduledPostTooltipCloseButton).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ChannelScreen.scheduledPostTooltipCloseButton.tap();
// * Verify on direct message channel screen for the new user // * Verify on direct message channel screen for the new user
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();
@ -123,17 +126,21 @@ describe('Channels - Create Direct Message', () => {
// # Open create direct message screen, search for the first new user and tap on the first new user item // # Open create direct message screen, search for the first new user and tap on the first new user item
await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.searchInput.replaceText(firstNewUser.username); await CreateDirectMessageScreen.searchInput.replaceText(firstNewUser.username);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.getUserItem(firstNewUser.id).tap(); await CreateDirectMessageScreen.getUserItem(firstNewUser.id).tap();
// * Verify the first new user is selected // * Verify the first new user is selected
await expect(CreateDirectMessageScreen.getSelectedUserDisplayName(firstNewUser.id)).toBeVisible(); await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(firstNewUser.id)).toBeVisible();
// # Search for the second new user and tap on the second new user item // # Search for the second new user and tap on the second new user item
await CreateDirectMessageScreen.searchInput.replaceText(secondNewUser.username); await CreateDirectMessageScreen.searchInput.replaceText(secondNewUser.username);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.getUserItem(secondNewUser.id).tap(); await CreateDirectMessageScreen.getUserItem(secondNewUser.id).tap();
// * Verify the second new user is selected // * Verify the second new user is selected
await expect(CreateDirectMessageScreen.getSelectedUserDisplayName(secondNewUser.id)).toBeVisible(); await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(secondNewUser.id)).toBeVisible();
// # Tap on start button // # Tap on start button
await CreateDirectMessageScreen.startButton.tap(); await CreateDirectMessageScreen.startButton.tap();
@ -154,6 +161,8 @@ describe('Channels - Create Direct Message', () => {
const searchTerm = 'blahblahblahblah'; const searchTerm = 'blahblahblahblah';
await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.searchInput.replaceText(searchTerm); await CreateDirectMessageScreen.searchInput.replaceText(searchTerm);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC);
// * Verify empty search state for create direct message // * Verify empty search state for create direct message
await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible(); await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible();

View file

@ -26,7 +26,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {isIos} from '@support/utils'; import {isAndroid, isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Edit Channel', () => { describe('Channels - Edit Channel', () => {
@ -112,6 +112,10 @@ describe('Channels - Edit Channel', () => {
await ChannelInfoScreen.toBeVisible(); await ChannelInfoScreen.toBeVisible();
await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(`${testChannel.display_name} name`); await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(`${testChannel.display_name} name`);
await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(`Channel purpose: ${testChannel.display_name.toLowerCase()} purpose`); await expect(ChannelInfoScreen.publicPrivateTitlePurpose).toHaveText(`Channel purpose: ${testChannel.display_name.toLowerCase()} purpose`);
if (isAndroid()) {
await ChannelInfoScreen.scrollView.scrollTo('top');
}
await expect(element(by.text(`Channel header: ${testChannel.display_name.toLowerCase()}\nheader1\nheader2`))).toBeVisible(); await expect(element(by.text(`Channel header: ${testChannel.display_name.toLowerCase()}\nheader1\nheader2`))).toBeVisible();
// # Go back to channel screen // # Go back to channel screen
@ -154,8 +158,11 @@ describe('Channels - Edit Channel', () => {
await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser1.username); await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser1.username);
await CreateDirectMessageScreen.getUserItem(testOtherUser1.id).tap(); await CreateDirectMessageScreen.getUserItem(testOtherUser1.id).tap();
await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser2.username); await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser2.username);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
await CreateDirectMessageScreen.getUserItem(testOtherUser2.id).tap(); await CreateDirectMessageScreen.getUserItem(testOtherUser2.id).tap();
await CreateDirectMessageScreen.startButton.tap(); await CreateDirectMessageScreen.startButton.tap();
await wait(timeouts.TWO_SEC);
// * Verify on group message channel screen // * Verify on group message channel screen
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();

View file

@ -25,7 +25,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {timeouts, wait} from '@support/utils'; import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Find Channels', () => { describe('Channels - Find Channels', () => {
@ -68,7 +68,8 @@ describe('Channels - Find Channels', () => {
await FindChannelsScreen.close(); await FindChannelsScreen.close();
}); });
it('MM-T4907_2 - should be able to find and navigate to a public channel', async () => { // TODO: Uncomment this test when the issue is fixed https://mattermost.atlassian.net/browse/MM-63814
(device.getPlatform() === 'ios' ? it : it.skip)('MM-T4907_2 - should be able to find and navigate to a public channel', async () => {
// # Open find channels screen and search for a public channel to navigate to // # Open find channels screen and search for a public channel to navigate to
await FindChannelsScreen.open(); await FindChannelsScreen.open();
await FindChannelsScreen.searchInput.replaceText(testChannel.display_name); await FindChannelsScreen.searchInput.replaceText(testChannel.display_name);
@ -98,7 +99,7 @@ describe('Channels - Find Channels', () => {
await FindChannelsScreen.close(); await FindChannelsScreen.close();
}); });
it('MM-T4907_4 - should be able to find direct and group message channels', async () => { (isIos() ? it : it.skip)('MM-T4907_4 - should be able to find direct and group message channels', async () => {
// # Create direct and group message channels, open find channels screen, and search for the direct message channel // # Create direct and group message channels, open find channels screen, and search for the direct message channel
const {user: testOtherUser1} = await User.apiCreateUser(siteOneUrl, {prefix: 'a'}); const {user: testOtherUser1} = await User.apiCreateUser(siteOneUrl, {prefix: 'a'});
await Team.apiAddUserToTeam(siteOneUrl, testOtherUser1.id, testTeam.id); await Team.apiAddUserToTeam(siteOneUrl, testOtherUser1.id, testTeam.id);
@ -127,7 +128,7 @@ describe('Channels - Find Channels', () => {
await ChannelScreen.back(); await ChannelScreen.back();
}); });
it('MM-T4907_5 - should be able to find an archived channel', async () => { (device.getPlatform() === 'ios' ? it : it.skip)('MM-T4907_5 - should be able to find an archived channel', async () => {
// # Archive a channel, open find channels screen, and search for the archived channel // # Archive a channel, open find channels screen, and search for the archived channel
const {channel: archivedChannel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id}); const {channel: archivedChannel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id});
await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, archivedChannel.id); await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, archivedChannel.id);
@ -171,6 +172,12 @@ describe('Channels - Find Channels', () => {
}); });
async function verifyDetailsOnChannelScreen(display_name: string) { async function verifyDetailsOnChannelScreen(display_name: string) {
try {
await ChannelScreen.scheduledPostTooltipCloseButton.tap();
} catch (error) {
// eslint-disable-next-line no-console
console.log('Element not visible, skipping click');
}
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(display_name); await expect(ChannelScreen.headerTitle).toHaveText(display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(display_name); await expect(ChannelScreen.introDisplayName).toHaveText(display_name);

View file

@ -17,16 +17,18 @@ import {
ServerScreen, ServerScreen,
ChannelInfoScreen, ChannelInfoScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getAdminAccount, getRandomId, timeouts} from '@support/utils'; import {getAdminAccount, getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Channels - Unarchive Channel', () => { describe('Channels - Unarchive Channel', () => {
const serverOneDisplayName = 'Server 1'; const serverOneDisplayName = 'Server 1';
beforeAll(async () => { beforeAll(async () => {
// # Log in to server as admin // # Log in to server as admin
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.loginAsAdmin(getAdminAccount()); await LoginScreen.loginAsAdmin(getAdminAccount());
await wait(timeouts.TWO_SEC);
}); });
beforeEach(async () => { beforeEach(async () => {
@ -45,6 +47,9 @@ describe('Channels - Unarchive Channel', () => {
await CreateOrEditChannelScreen.openCreateChannel(); await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName); await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName);
await CreateOrEditChannelScreen.createButton.tap(); await CreateOrEditChannelScreen.createButton.tap();
await wait(timeouts.TWO_SEC);
await expect(ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount).toBeVisible();
await ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount.tap();
await ChannelInfoScreen.open(); await ChannelInfoScreen.open();
await ChannelInfoScreen.archivePublicChannel({confirm: true}); await ChannelInfoScreen.archivePublicChannel({confirm: true});

View file

@ -85,7 +85,7 @@ describe('Messaging - At-Mention', () => {
await ChannelScreen.back(); await ChannelScreen.back();
}); });
it.skip('MM-T4874_2 - should display confirmation dialog when posting @all, @channel, and @here -- UNSTABLE', async () => { it('MM-T4874_2 - should display confirmation dialog when posting @all, @channel, and @here', async () => {
// # Add more users to the channel, open a channel screen, and post @all // # Add more users to the channel, open a channel screen, and post @all
[...Array(3).keys()].forEach(async (key) => { [...Array(3).keys()].forEach(async (key) => {
const {user} = await User.apiCreateUser(siteOneUrl, {prefix: `a-${key}-`}); const {user} = await User.apiCreateUser(siteOneUrl, {prefix: `a-${key}-`});

View file

@ -152,7 +152,7 @@ describe('Messaging - Emojis and Reactions', () => {
await EmojiPickerScreen.open(); await EmojiPickerScreen.open();
// * Verify emojis exist in recently used section // * Verify emojis exist in recently used section
await expect(element(by.text('RECENTLY USED'))).toExist(); await expect(element(by.text('RECENTLY USED')).atIndex(0)).toBeVisible();
await expect(element(by.text('🦊')).atIndex(0)).toExist(); await expect(element(by.text('🦊')).atIndex(0)).toExist();
await expect(element(by.text('🐶')).atIndex(0)).toExist(); await expect(element(by.text('🐶')).atIndex(0)).toExist();
@ -175,5 +175,9 @@ describe('Messaging - Emojis and Reactions', () => {
// * Verify empty search state for emoji picker // * Verify empty search state for emoji picker
await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible(); await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible();
await expect(element(by.text('Check the spelling or try another search.'))).toBeVisible(); await expect(element(by.text('Check the spelling or try another search.'))).toBeVisible();
// # Go back to channel list screen
await EmojiPickerScreen.close();
await ChannelScreen.back();
}); });
}); });

View file

@ -23,7 +23,7 @@ import {
PostOptionsScreen, PostOptionsScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getRandomId, timeouts} from '@support/utils'; import {getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Messaging - Follow and Unfollow Message', () => { describe('Messaging - Follow and Unfollow Message', () => {
@ -62,19 +62,21 @@ describe('Messaging - Follow and Unfollow Message', () => {
await expect(postListPostItem).toBeVisible(); await expect(postListPostItem).toBeVisible();
// # Open post options for message and tap on follow message option // # Open post options for message and tap on follow message option
await ChannelScreen.openPostOptionsFor(post.id, message); await postListPostItem.longPress(timeouts.ONE_SEC);
await PostOptionsScreen.followThreadOption.tap(); await PostOptionsScreen.followThreadOption.tap();
// * Verify message is followed by user via post footer // * Verify message is followed by user via post footer
const {postListPostItemFooterFollowingButton} = ChannelScreen.getPostListPostItem(post.id, message); const {postListPostItemFooterFollowingButton} = ChannelScreen.getPostListPostItem(post.id, message);
await waitFor(postListPostItemFooterFollowingButton).toBeVisible().withTimeout(timeouts.TWO_SEC); await waitFor(postListPostItemFooterFollowingButton).toBeVisible().withTimeout(timeouts.TEN_SEC);
// # Open post options for message and tap on unfollow message option // # Open post options for message and tap on unfollow message option
await ChannelScreen.openPostOptionsFor(post.id, message); await ChannelScreen.openPostOptionsFor(post.id, message);
await PostOptionsScreen.followingThreadOption.tap(); await PostOptionsScreen.followingThreadOption.tap();
// * Verify message is not followed by user via post footer // * Verify message is not followed by user via post footer
await waitFor(postListPostItemFooterFollowingButton).not.toBeVisible().withTimeout(timeouts.TWO_SEC); await waitFor(postListPostItemFooterFollowingButton).not.toBeVisible().withTimeout(timeouts.TEN_SEC);
await wait(timeouts.TWO_SEC);
// # Go back to channel list screen // # Go back to channel list screen
await ChannelScreen.back(); await ChannelScreen.back();

View file

@ -48,7 +48,8 @@ describe('Messaging - Markdown Latex', () => {
await HomeScreen.logout(); await HomeScreen.logout();
}); });
it('MM-T4900_1 - should be able to display markdown latex code block', async () => { // TODO: Uncomment the test when the issue is fixed https://mattermost.atlassian.net/browse/MM-63817
it.skip('MM-T4900_1 - should be able to display markdown latex code block', async () => {
// # Open a channel screen and post a markdown latex code block // # Open a channel screen and post a markdown latex code block
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const message = 'X_k = \sum_{n=0}^{2N-1} x_n \cos \left[\frac{\pi}{N} \left(n+\frac{1}{2}+\frac{N}{2}\right) \left(k+\frac{1}{2}\right) \right]'; const message = 'X_k = \sum_{n=0}^{2N-1} x_n \cos \left[\frac{\pi}{N} \left(n+\frac{1}{2}+\frac{N}{2}\right) \left(k+\frac{1}{2}\right) \right]';

View file

@ -22,6 +22,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {isAndroid, isIos} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Messaging - Markdown List', () => { describe('Messaging - Markdown List', () => {
@ -48,7 +49,7 @@ describe('Messaging - Markdown List', () => {
await HomeScreen.logout(); await HomeScreen.logout();
}); });
it.skip('MM-T4894_1 - should be able to display markdown bullet list -- UNSTABLE', async () => { it('MM-T4894_1 - should be able to display markdown bullet list', async () => {
// # Open a channel screen and post a markdown bullet list // # Open a channel screen and post a markdown bullet list
const item1 = 'item one'; const item1 = 'item one';
const item2 = 'item two'; const item2 = 'item two';
@ -61,15 +62,24 @@ describe('Messaging - Markdown List', () => {
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItemListItem, postListPostItemListItemBullet} = ChannelScreen.getPostListPostItem(post.id); const {postListPostItemListItem, postListPostItemListItemBullet} = ChannelScreen.getPostListPostItem(post.id);
await expect(postListPostItemListItem.atIndex(0)).toBeVisible(); await expect(postListPostItemListItem.atIndex(0)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('•');
await expect(element(by.text(item1))).toBeVisible(); await expect(element(by.text(item1))).toBeVisible();
await expect(postListPostItemListItem.atIndex(1)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(1)).toHaveText('•');
await expect(element(by.text(item2))).toBeVisible(); await expect(element(by.text(item2))).toBeVisible();
await expect(postListPostItemListItem.atIndex(2)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('◦');
await expect(element(by.text(item2SubPoint))).toBeVisible(); await expect(element(by.text(item2SubPoint))).toBeVisible();
if (isAndroid()) {
await expect(postListPostItemListItem.atIndex(0)).toHaveLabel(`${item1}`);
}
if (isIos()) {
await expect(postListPostItemListItem.atIndex(0)).toBeVisible();
await expect(postListPostItemListItem.atIndex(1)).toBeVisible();
await expect(postListPostItemListItem.atIndex(2)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('•');
await expect(postListPostItemListItemBullet.atIndex(1)).toHaveText('•');
await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('◦');
}
// # Go back to channel list screen // # Go back to channel list screen
await ChannelScreen.back(); await ChannelScreen.back();
}); });
@ -89,13 +99,20 @@ describe('Messaging - Markdown List', () => {
await expect(postListPostItemListItem.atIndex(0)).toBeVisible(); await expect(postListPostItemListItem.atIndex(0)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('1.'); await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('1.');
await expect(element(by.text(item1))).toBeVisible(); await expect(element(by.text(item1))).toBeVisible();
await expect(postListPostItemListItem.atIndex(1)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(1)).toHaveText('2.');
await expect(element(by.text(item2))).toBeVisible(); await expect(element(by.text(item2))).toBeVisible();
await expect(postListPostItemListItem.atIndex(2)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('3.');
await expect(element(by.text(item3))).toBeVisible(); await expect(element(by.text(item3))).toBeVisible();
if (isAndroid()) {
await expect(postListPostItemListItem.atIndex(0)).toHaveLabel(`1. ${item1}`);
}
if (isIos()) {
await expect(postListPostItemListItem.atIndex(1)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(1)).toHaveText('2.');
await expect(postListPostItemListItem.atIndex(2)).toBeVisible();
await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('3.');
}
// # Go back to channel list screen // # Go back to channel list screen
await ChannelScreen.back(); await ChannelScreen.back();
}); });

View file

@ -23,6 +23,7 @@ import {
ServerScreen, ServerScreen,
TableScreen, TableScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {isIos} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Messaging - Markdown Table', () => { describe('Messaging - Markdown Table', () => {
@ -56,11 +57,11 @@ describe('Messaging - Markdown Table', () => {
'|:---|:---|:---|\n' + '|:---|:---|:---|\n' +
'| 1 | Name | Toast |\n' + '| 1 | Name | Toast |\n' +
'| 2 | Name | Server |\n'; '| 2 | Name | Server |\n';
await ChannelScreen.open(channelsCategory, testChannel.name);
await Post.apiCreatePost(siteOneUrl, { await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id, channelId: testChannel.id,
message: markdownTable, message: markdownTable,
}); });
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify markdown table is displayed // * Verify markdown table is displayed
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -77,11 +78,11 @@ describe('Messaging - Markdown Table', () => {
'| Left header that wraps | Center header that wraps | Right header that wraps |\n' + '| Left header that wraps | Center header that wraps | Right header that wraps |\n' +
'| :-- | :-: | --: |\n' + '| :-- | :-: | --: |\n' +
'| Left text that wraps row | Center text that wraps row | Right text that wraps row |\n'; '| Left text that wraps row | Center text that wraps row | Right text that wraps row |\n';
await ChannelScreen.open(channelsCategory, testChannel.name);
await Post.apiCreatePost(siteOneUrl, { await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id, channelId: testChannel.id,
message: markdownTable, message: markdownTable,
}); });
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify table is displayed with long text wrapped properly // * Verify table is displayed with long text wrapped properly
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -119,11 +120,11 @@ describe('Messaging - Markdown Table', () => {
'| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' + '| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' +
'| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(7) + '| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(7) +
'| Left | Center | Right | Right | Left | Center | Right | Right HS last |\n'; '| Left | Center | Right | Right | Left | Center | Right | Right HS last |\n';
await ChannelScreen.open(channelsCategory, testChannel.name);
await Post.apiCreatePost(siteOneUrl, { await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id, channelId: testChannel.id,
message: markdownTable, message: markdownTable,
}); });
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify table is displayed with some right columns not visible // * Verify table is displayed with some right columns not visible
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -156,11 +157,11 @@ describe('Messaging - Markdown Table', () => {
'| :-- | :-: | --: |\n' + '| :-- | :-: | --: |\n' +
'| Left | Center | Right |\n'.repeat(30) + '| Left | Center | Right |\n'.repeat(30) +
'| Left | Center | Right VS last |\n'; '| Left | Center | Right VS last |\n';
await ChannelScreen.open(channelsCategory, testChannel.name);
await Post.apiCreatePost(siteOneUrl, { await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id, channelId: testChannel.id,
message: markdownTable, message: markdownTable,
}); });
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify table is displayed with some bottom rows not visible // * Verify table is displayed with some bottom rows not visible
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -178,9 +179,13 @@ describe('Messaging - Markdown Table', () => {
// * Verify table screen is scrollable to the bottom // * Verify table screen is scrollable to the bottom
const expectedElement = element(by.text('Right VS last')); const expectedElement = element(by.text('Right VS last'));
await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(50, 'down'); if (isIos()) {
await expect(element(by.text('Header VS last'))).not.toBeVisible(); await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'down');
await expect(expectedElement).toBeVisible(); await expect(element(by.text('Header VS last'))).not.toBeVisible();
await expect(expectedElement).toBeVisible();
} else {
await expect(expectedElement).toExist();
}
// # Go back to channel list screen // # Go back to channel list screen
await TableScreen.back(); await TableScreen.back();
@ -194,11 +199,11 @@ describe('Messaging - Markdown Table', () => {
'| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' + '| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' +
'| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(30) + '| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(30) +
'| Left | Center | Right | Right | Left | Center | Right | Right last |\n'; '| Left | Center | Right | Right | Left | Center | Right | Right last |\n';
await ChannelScreen.open(channelsCategory, testChannel.name);
await Post.apiCreatePost(siteOneUrl, { await Post.apiCreatePost(siteOneUrl, {
channelId: testChannel.id, channelId: testChannel.id,
message: markdownTable, message: markdownTable,
}); });
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify table is displayed with some right columns and bottom rows not visible // * Verify table is displayed with some right columns and bottom rows not visible
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -215,13 +220,16 @@ describe('Messaging - Markdown Table', () => {
await expect(element(by.text('Right last'))).not.toBeVisible(); await expect(element(by.text('Right last'))).not.toBeVisible();
// * Verify table screen is scrollable to the right and scrollable to the bottom // * Verify table screen is scrollable to the right and scrollable to the bottom
await TableScreen.tableScrollView.scrollTo('right'); await waitFor(element(by.text('Header last'))).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'right');
await expect(element(by.text('Header last'))).toBeVisible();
await expect(element(by.text('Right last'))).not.toBeVisible(); await expect(element(by.text('Right last'))).not.toBeVisible();
const expectedElement = element(by.text('Right last')); const expectedElement = element(by.text('Right last'));
await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(50, 'down'); if (isIos()) {
await expect(element(by.text('Header last'))).not.toBeVisible(); await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'down');
await expect(expectedElement).toBeVisible(); await expect(element(by.text('Header last'))).not.toBeVisible();
await expect(expectedElement).toBeVisible();
} else {
await expect(expectedElement).toExist();
}
// # Go back to channel list screen // # Go back to channel list screen
await TableScreen.back(); await TableScreen.back();

View file

@ -68,7 +68,7 @@ describe('Messaging - Pin and Unpin Message', () => {
await PostOptionsScreen.pinPostOption.tap(); await PostOptionsScreen.pinPostOption.tap();
// * Verify pinned text is displayed on the post pre-header // * Verify pinned text is displayed on the post pre-header
await wait(timeouts.ONE_SEC); await wait(timeouts.TEN_SEC);
const {postListPostItemPreHeaderText} = ChannelScreen.getPostListPostItem(post.id, message); const {postListPostItemPreHeaderText} = ChannelScreen.getPostListPostItem(post.id, message);
await expect(postListPostItemPreHeaderText).toHaveText(pinnedText); await expect(postListPostItemPreHeaderText).toHaveText(pinnedText);
@ -77,7 +77,7 @@ describe('Messaging - Pin and Unpin Message', () => {
await PostOptionsScreen.unpinPostOption.tap(); await PostOptionsScreen.unpinPostOption.tap();
// * Verify pinned text is not displayed on the post pre-header // * Verify pinned text is not displayed on the post pre-header
await wait(timeouts.ONE_SEC); await wait(timeouts.TEN_SEC);
await expect(postListPostItemPreHeaderText).not.toBeVisible(); await expect(postListPostItemPreHeaderText).not.toBeVisible();
// # Go back to channel list screen // # Go back to channel list screen
@ -96,7 +96,7 @@ describe('Messaging - Pin and Unpin Message', () => {
await PostOptionsScreen.pinPostOption.tap(); await PostOptionsScreen.pinPostOption.tap();
// * Verify pinned text is displayed on the post pre-header // * Verify pinned text is displayed on the post pre-header
await wait(timeouts.ONE_SEC); await wait(timeouts.TEN_SEC);
const {postListPostItemPreHeaderText} = ThreadScreen.getPostListPostItem(post.id, message); const {postListPostItemPreHeaderText} = ThreadScreen.getPostListPostItem(post.id, message);
await expect(postListPostItemPreHeaderText).toHaveText(pinnedText); await expect(postListPostItemPreHeaderText).toHaveText(pinnedText);
@ -105,7 +105,7 @@ describe('Messaging - Pin and Unpin Message', () => {
await PostOptionsScreen.unpinPostOption.tap(); await PostOptionsScreen.unpinPostOption.tap();
// * Verify pinned text is not displayed on the post pre-header // * Verify pinned text is not displayed on the post pre-header
await wait(timeouts.ONE_SEC); await wait(timeouts.TEN_SEC);
await expect(postListPostItemPreHeaderText).not.toBeVisible(); await expect(postListPostItemPreHeaderText).not.toBeVisible();
// # Go back to channel list screen // # Go back to channel list screen

View file

@ -0,0 +1,232 @@
// 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,
Post,
Setup,
Team,
User,
} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
} from '@support/test_config';
import {
ChannelListScreen,
ChannelScreen,
HomeScreen,
ScheduleMessageScreen,
LoginScreen,
ServerScreen,
DraftScreen,
ThreadScreen,
} from '@support/ui/screen';
import {isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Scheduled Draft,', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
let testChannel: any;
let testTeam: any;
let testUser: any;
let testOtherUser: any;
beforeAll(async () => {
const {channel, team, user} = await Setup.apiInit(siteOneUrl);
testChannel = channel;
testTeam = team;
testUser = user;
({user: testOtherUser} = await User.apiCreateUser(siteOneUrl));
await Team.apiAddUserToTeam(siteOneUrl, testOtherUser.id, testTeam.id);
await Channel.apiAddUserToChannel(siteOneUrl, testOtherUser.id, testChannel.id);
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(testUser);
});
beforeEach(async () => {
// * Verify on channel list screen
await ChannelListScreen.toBeVisible();
});
afterAll(async () => {
// # Log out
await HomeScreen.logout();
});
it('MM-T5762 should be able to create a scheduled message', async () => {
const scheduledMessageText = 'Scheduled Message In a channel';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.enterMessageToSchedule(scheduledMessageText);
await ChannelScreen.longPressSendButton();
await chooseScheduleMessageDate();
await wait(timeouts.TWO_SEC);
await ChannelScreen.verifyScheduledDraftInfoInChannel();
await verifyScheduledCountOnChannelListScreen('1');
// # Open scheduled message screen and verify count
await ChannelListScreen.draftsButton.tap();
await ScheduleMessageScreen.clickScheduledTab();
await ScheduleMessageScreen.verifyCountOnScheduledTab('1');
await ScheduleMessageScreen.assertScheduledMessageExists(scheduledMessageText);
await cleanupDrafts();
await DraftScreen.backButton.tap();
});
it('MM-T5767 should be able to create a scheduled message under a threaded post', async () => {
const parentMessage = 'Root Post for Scheduled Message';
const scheduledMessageText = 'Scheduled Message In a channel';
await ChannelScreen.open(channelsCategory, testChannel.name);
await waitFor(ChannelScreen.postInput).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await ChannelScreen.postMessage(parentMessage);
// # Open reply thread
const {post: parentPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
await waitFor(ThreadScreen.postInput).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await ThreadScreen.enterMessageToSchedule(scheduledMessageText);
await ThreadScreen.longPressSendButton();
await chooseScheduleMessageDate();
await ChannelScreen.verifyScheduledDraftInfoInChannel(true);
await ThreadScreen.back();
await verifyScheduledCountOnChannelListScreen('1');
// # Open scheduled message screen and verify count
await ChannelListScreen.draftsButton.tap();
await ScheduleMessageScreen.clickScheduledTab();
await ScheduleMessageScreen.verifyCountOnScheduledTab('1');
await ScheduleMessageScreen.assertScheduledMessageExists(scheduledMessageText);
await DraftScreen.openDraftPostActions();
await DraftScreen.sendDraft();
await DraftScreen.backButton.tap();
// * Verify the scheduled message is shown in the channel
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.openReplyThreadFor(parentPost.id, parentMessage);
await expect(element(by.text(scheduledMessageText))).toBeVisible();
await ThreadScreen.back();
await ChannelScreen.back();
});
it('MM-T5731 should be able to Delete a scheduled Message', async () => {
const scheduledMessageText = 'Scheduled Message In a channel';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.enterMessageToSchedule(scheduledMessageText);
await ChannelScreen.longPressSendButton();
await chooseScheduleMessageDate();
await ChannelScreen.verifyScheduledDraftInfoInChannel();
await verifyScheduledCountOnChannelListScreen('1');
// # Open scheduled message screen and verify count
await ChannelListScreen.draftsButton.tap();
await ScheduleMessageScreen.clickScheduledTab();
await ScheduleMessageScreen.verifyCountOnScheduledTab('1');
await ScheduleMessageScreen.assertScheduledMessageExists(scheduledMessageText);
await DraftScreen.openDraftPostActions();
await ScheduleMessageScreen.deleteScheduledMessageFromDraftActions();
await DraftScreen.backButton.tap();
await verifyScheduledScheduledMessageDoesNotExist();
});
it('MM-T5730 should be able to Send a scheduled Message', async () => {
const scheduledMessageText = 'Scheduled Message In a channel';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.enterMessageToSchedule(scheduledMessageText);
await ChannelScreen.longPressSendButton();
await chooseScheduleMessageDate();
await verifyScheduledCountOnChannelListScreen('1');
// # Open scheduled message screen and verify count
await ChannelListScreen.draftsButton.tap();
await ScheduleMessageScreen.clickScheduledTab();
await ScheduleMessageScreen.verifyCountOnScheduledTab('1');
await ScheduleMessageScreen.assertScheduledMessageExists(scheduledMessageText);
await DraftScreen.openDraftPostActions();
await DraftScreen.sendDraft();
await DraftScreen.backButton.tap();
// * Verify the scheduled message is shown in the channel
await ChannelScreen.open(channelsCategory, testChannel.name);
// * Verify message is added to post list
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
const {postListPostItem} = ChannelScreen.getPostListPostItem(post.id, scheduledMessageText);
await expect(postListPostItem).toBeVisible();
await ChannelScreen.back();
await verifyScheduledScheduledMessageDoesNotExist();
});
it('MM-T5720 should be able to Reschedule a scheduled Message', async () => {
const scheduledMessageText = 'Scheduled Message In a channel';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.enterMessageToSchedule(scheduledMessageText);
await ChannelScreen.longPressSendButton();
await chooseScheduleMessageDate();
await ChannelScreen.verifyScheduledDraftInfoInChannel();
await verifyScheduledCountOnChannelListScreen('1');
// # Open scheduled message screen and verify count
await ChannelListScreen.draftsButton.tap();
await ScheduleMessageScreen.clickScheduledTab();
await ScheduleMessageScreen.verifyCountOnScheduledTab('1');
await ScheduleMessageScreen.assertScheduledMessageExists(scheduledMessageText);
await ScheduleMessageScreen.assertScheduleTimeTextIsVisible(await ScheduleMessageScreen.nextMonday());
if (isIos()) {
// Andoid uses native date picker which is not supported by detox asit cannot interact with native UI
await DraftScreen.openDraftPostActions();
await ScheduleMessageScreen.clickRescheduleOption();
await ScheduleMessageScreen.selectDateTime();
}
// Clean up drafts
await DraftScreen.openDraftPostActions();
await ScheduleMessageScreen.deleteScheduledMessageFromDraftActions();
await DraftScreen.backButton.tap();
});
async function cleanupDrafts() {
// # Clean up drafts
await DraftScreen.openDraftPostActions();
await ScheduleMessageScreen.deleteScheduledMessageFromDraftActions();
}
async function verifyScheduledCountOnChannelListScreen(draftCount: string) {
await ChannelScreen.back();
await ChannelListScreen.draftsButton.toBeVisible();
await expect(element(by.id(ChannelListScreen.testID.scheduledMessageCountListScreen))).toHaveText(draftCount);
}
async function verifyScheduledScheduledMessageDoesNotExist() {
await ChannelListScreen.draftsButton.toNotBeVisible();
await expect(element(by.id(ChannelListScreen.testID.scheduledMessageCountListScreen))).not.toExist();
}
async function chooseScheduleMessageDate() {
// # The bottom sheet info will not show `Tomorrow` if today is Friday or Saturday. Always schedule for Monday.
const today = new Date().getDay();
if (today === 1) { // 1 represents Monday
await ChannelScreen.scheduleMessageForNextMonday();
} else {
await ChannelScreen.scheduleMessageForMonday();
}
await ChannelScreen.clickOnScheduledMessage();
}
});

View file

@ -96,7 +96,7 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.searchModifierFrom.tap(); await SearchMessagesScreen.searchModifierFrom.tap();
await SearchMessagesScreen.searchInput.typeText(testUser.username); await SearchMessagesScreen.searchInput.typeText(testUser.username);
const {atMentionItem} = Autocomplete.getAtMentionItem(testUser.id); const {atMentionItem} = Autocomplete.getAtMentionItem(testUser.id);
await waitFor(atMentionItem).toBeVisible().withTimeout(2000); await waitFor(atMentionItem).toBeVisible().withTimeout(timeouts.TWO_SEC);
await atMentionItem.tap(); await atMentionItem.tap();
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
@ -126,6 +126,7 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.searchModifierIn.tap(); await SearchMessagesScreen.searchModifierIn.tap();
await SearchMessagesScreen.searchInput.typeText(testChannel.name); await SearchMessagesScreen.searchInput.typeText(testChannel.name);
const {channelMentionItem} = Autocomplete.getChannelMentionItem(testChannel.name); const {channelMentionItem} = Autocomplete.getChannelMentionItem(testChannel.name);
await waitFor(channelMentionItem).toBeVisible().withTimeout(timeouts.TWO_SEC);
await channelMentionItem.tap(); await channelMentionItem.tap();
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
@ -166,6 +167,7 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
// * Verify search results do not contain messages with excluded term // * Verify search results do not contain messages with excluded term
await wait(timeouts.TWO_SEC);
await expect(nonExcludedPostListPostItem).toBeVisible(); await expect(nonExcludedPostListPostItem).toBeVisible();
await expect(excludedPostListPostItem).not.toBeVisible(); await expect(excludedPostListPostItem).not.toBeVisible();
@ -198,11 +200,15 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.searchModifierPhrases.tap(); await SearchMessagesScreen.searchModifierPhrases.tap();
await SearchMessagesScreen.searchInput.tapBackspaceKey(); await SearchMessagesScreen.searchInput.tapBackspaceKey();
await SearchMessagesScreen.searchInput.typeText(messageWithIncludedTerm); await SearchMessagesScreen.searchInput.typeText(messageWithIncludedTerm);
// # Collapse the keyboard
await element(by.id('search.modifier.header')).tap();
await SearchMessagesScreen.searchModifierPhrases.tap(); await SearchMessagesScreen.searchModifierPhrases.tap();
await SearchMessagesScreen.searchInput.tapBackspaceKey(); await SearchMessagesScreen.searchInput.tapBackspaceKey();
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
// * Verify search results only contain messages with included term // * Verify search results only contain messages with included term
await wait(timeouts.TWO_SEC);
await expect(nonIncludedPostListPostItem).not.toBeVisible(); await expect(nonIncludedPostListPostItem).not.toBeVisible();
await expect(includedPostListPostItem).toBeVisible(); await expect(includedPostListPostItem).toBeVisible();
@ -223,17 +229,10 @@ describe('Search - Search Messages', () => {
// * Verify on search messages screen // * Verify on search messages screen
await SearchMessagesScreen.toBeVisible(); await SearchMessagesScreen.toBeVisible();
// # Tap on from-search-modifier, type in username, tap on user at-mention autocomplete, tap on in-search-modifier, type in channel, tap on channel mention autocomplete, and tap on search key // # Tap on from-search-modifier, type in username
await SearchMessagesScreen.searchModifierFrom.tap(); await SearchMessagesScreen.searchInput.typeText(`from: ${testUser.username} channel: ${testChannel.name}`);
await SearchMessagesScreen.searchInput.typeText(testUser.username);
const {atMentionItem} = Autocomplete.getAtMentionItem(testUser.id);
await atMentionItem.tap();
await SearchMessagesScreen.searchModifierIn.tap();
await wait(timeouts.ONE_SEC);
await SearchMessagesScreen.searchInput.typeText(testChannel.name);
const {channelMentionItem} = Autocomplete.getChannelMentionItem(testChannel.name);
await channelMentionItem.tap();
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
// * Verify search results only contain messages from user in channel // * Verify search results only contain messages from user in channel
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -261,6 +260,7 @@ describe('Search - Search Messages', () => {
// # Type in a search term that will yield results and tap on search key // # Type in a search term that will yield results and tap on search key
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TEN_SEC);
// * Verify search results contain searched message // * Verify search results contain searched message
const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
@ -314,6 +314,7 @@ describe('Search - Search Messages', () => {
// # Type in a search term that will yield results for second team and tap on search key // # Type in a search term that will yield results for second team and tap on search key
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
// * Verify search results contain searched message // * Verify search results contain searched message
const {postListPostItem} = SearchMessagesScreen.getPostListPostItem(post.id, message); const {postListPostItem} = SearchMessagesScreen.getPostListPostItem(post.id, message);
@ -321,6 +322,7 @@ describe('Search - Search Messages', () => {
// # Tap on team picker button and tap on first team option // # Tap on team picker button and tap on first team option
await SearchMessagesScreen.teamPickerButton.tap(); await SearchMessagesScreen.teamPickerButton.tap();
await wait(timeouts.TWO_SEC);
await TeamDropdownMenuScreen.getTeamIcon(testTeam.id).tap(); await TeamDropdownMenuScreen.getTeamIcon(testTeam.id).tap();
// * Verify search results do not contain searched message // * Verify search results do not contain searched message
@ -347,6 +349,7 @@ describe('Search - Search Messages', () => {
const searchTerm = getRandomId(); const searchTerm = getRandomId();
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
// * Verify empty search state for search messages // * Verify empty search state for search messages
await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible(); await expect(element(by.text(`No matches found for “${searchTerm}`))).toBeVisible();
@ -373,6 +376,8 @@ describe('Search - Search Messages', () => {
// # Type in a search term that will yield results, tap on search key, open post options for searched message, and tap on edit option // # Type in a search term that will yield results, tap on search key, open post options for searched message, and tap on edit option
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message);
await PostOptionsScreen.editPostOption.tap(); await PostOptionsScreen.editPostOption.tap();
@ -442,6 +447,7 @@ describe('Search - Search Messages', () => {
// # Type in a search term that will yield results, tap on search key, open post options for searched message, tap on save option, and open saved messages screen // # Type in a search term that will yield results, tap on search key, open post options for searched message, tap on save option, and open saved messages screen
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message);
await PostOptionsScreen.savePostOption.tap(); await PostOptionsScreen.savePostOption.tap();
@ -482,6 +488,8 @@ describe('Search - Search Messages', () => {
// # Type in a search term that will yield results, tap on search key, open post options for searched message, tap on pin to channel option, go back to channel list screen, open the channel screen where searched message is posted, open channel info screen, and open pinned messages screen // # Type in a search term that will yield results, tap on search key, open post options for searched message, tap on pin to channel option, go back to channel list screen, open the channel screen where searched message is posted, open channel info screen, and open pinned messages screen
await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.typeText(searchTerm);
await SearchMessagesScreen.searchInput.tapReturnKey(); await SearchMessagesScreen.searchInput.tapReturnKey();
await wait(timeouts.TWO_SEC);
const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id);
await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message);
await PostOptionsScreen.pinPostOption.tap(); await PostOptionsScreen.pinPostOption.tap();

View file

@ -78,7 +78,11 @@ describe('Server Login - Connect to Server', () => {
// * Verify invalid url error // * Verify invalid url error
await waitFor(serverUrlInputError).toExist().withTimeout(timeouts.FOUR_SEC); await waitFor(serverUrlInputError).toExist().withTimeout(timeouts.FOUR_SEC);
await expect(serverUrlInputError).toHaveText('URLSessionTask failed with error: A server with the specified hostname could not be found.'); const expectedErrorText = isIos()
? 'URLSessionTask failed with error: A server with the specified hostname could not be found.'
: 'Unable to resolve host "invalid": No address associated with hostname';
await expect(serverUrlInputError).toHaveText(expectedErrorText);
}); });
it('MM-T4676_4 - should show connection error on invalid ssl or invalid host', async () => { it('MM-T4676_4 - should show connection error on invalid ssl or invalid host', async () => {

View file

@ -17,6 +17,7 @@ import {
LoginScreen, LoginScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Server Login - Login by Email', () => { describe('Server Login - Login by Email', () => {
@ -61,14 +62,16 @@ describe('Server Login - Login by Email', () => {
it('MM-T4677_2 - should show disabled signin button on empty username or password', async () => { it('MM-T4677_2 - should show disabled signin button on empty username or password', async () => {
// # Log in with empty username and non-empty password // # Log in with empty username and non-empty password
await usernameInput.clearText(); await usernameInput.clearText();
await passwordInput.typeText('password'); await passwordInput.replaceText('password');
await wait(timeouts.TWO_SEC);
// * Verify signin button is disabled // * Verify signin button is disabled
await expect(signinButtonDisabled).toBeVisible(); await expect(signinButtonDisabled).toBeVisible();
// # Log in with non-empty username and empty password // # Log in with non-empty username and empty password
await usernameInput.typeText('username'); await usernameInput.replaceText('username');
await passwordInput.clearText(); await passwordInput.clearText();
await LoginScreen.loginFormInfoText.tap();
// * Verify signin button is disabled // * Verify signin button is disabled
await expect(signinButtonDisabled).toBeVisible(); await expect(signinButtonDisabled).toBeVisible();
@ -76,8 +79,10 @@ describe('Server Login - Login by Email', () => {
it('MM-T4677_3 - should show incorrect combination error on incorrect credentials', async () => { it('MM-T4677_3 - should show incorrect combination error on incorrect credentials', async () => {
// # Log in with incorrect credentials // # Log in with incorrect credentials
await usernameInput.typeText('username'); await usernameInput.replaceText('username');
await passwordInput.typeText('password'); await passwordInput.replaceText('password');
await wait(timeouts.TWO_SEC);
await LoginScreen.loginFormInfoText.tap();
await signinButton.tap(); await signinButton.tap();
// * Verify incorrect combination error // * Verify incorrect combination error
@ -87,9 +92,12 @@ describe('Server Login - Login by Email', () => {
it('MM-T4677_4 - should show channel list screen on successful login', async () => { it('MM-T4677_4 - should show channel list screen on successful login', async () => {
// # Log in to server with correct credentials // # Log in to server with correct credentials
const {team, user} = await Setup.apiInit(siteOneUrl); const {team, user} = await Setup.apiInit(siteOneUrl);
await usernameInput.typeText(user.newUser.username); await usernameInput.replaceText(user.newUser.username);
await passwordInput.typeText(user.newUser.password); await passwordInput.replaceText(user.newUser.password);
await LoginScreen.loginFormInfoText.tap();
await signinButton.tap(); await signinButton.tap();
await wait(timeouts.TWO_SEC);
// * Verify on channel list screen and channel list header shows team display name and server display name // * Verify on channel list screen and channel list header shows team display name and server display name
await ChannelListScreen.toBeVisible(); await ChannelListScreen.toBeVisible();

View file

@ -87,8 +87,9 @@ describe('Server Login - Server List', () => {
// # Add a second server and log in to the second server // # Add a second server and log in to the second server
await User.apiAdminLogin(siteTwoUrl); await User.apiAdminLogin(siteTwoUrl);
({user: serverTwoUser} = await Setup.apiInit(siteTwoUrl)); ({user: serverTwoUser} = await Setup.apiInit(siteTwoUrl));
await wait(timeouts.TWO_SEC);
await ServerListScreen.addServerButton.tap(); await ServerListScreen.addServerButton.tap();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(); await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName); await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
await LoginScreen.login(serverTwoUser); await LoginScreen.login(serverTwoUser);
@ -112,8 +113,9 @@ describe('Server Login - Server List', () => {
// # Add a third server and log in to the third server // # Add a third server and log in to the third server
await User.apiAdminLogin(siteThreeUrl); await User.apiAdminLogin(siteThreeUrl);
({user: serverThreeUser} = await Setup.apiInit(siteThreeUrl)); ({user: serverThreeUser} = await Setup.apiInit(siteThreeUrl));
await wait(timeouts.TWO_SEC);
await ServerListScreen.addServerButton.tap(); await ServerListScreen.addServerButton.tap();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(); await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverThreeUrl, serverThreeDisplayName); await ServerScreen.connectToServer(serverThreeUrl, serverThreeDisplayName);
await LoginScreen.login(serverThreeUser); await LoginScreen.login(serverThreeUser);
@ -123,6 +125,7 @@ describe('Server Login - Server List', () => {
// # Open server list screen // # Open server list screen
await ServerListScreen.open(); await ServerListScreen.open();
await wait(timeouts.TWO_SEC);
if (isIos()) { if (isIos()) {
await ServerListScreen.serverListTitle.swipe('up'); await ServerListScreen.serverListTitle.swipe('up');
} else if (isAndroid()) { } else if (isAndroid()) {
@ -183,7 +186,7 @@ describe('Server Login - Server List', () => {
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
await waitFor(ServerListScreen.getServerItemActive(serverOneDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC); await waitFor(ServerListScreen.getServerItemActive(serverOneDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left'); await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left', 'slow');
await ServerListScreen.getServerItemEditOption(serverOneDisplayName).tap(); await ServerListScreen.getServerItemEditOption(serverOneDisplayName).tap();
// * Verify on edit server screen // * Verify on edit server screen
@ -209,7 +212,7 @@ describe('Server Login - Server List', () => {
await expect(ServerListScreen.getServerItemActive(newServerOneDisplayName)).toBeVisible(); await expect(ServerListScreen.getServerItemActive(newServerOneDisplayName)).toBeVisible();
// # Revert back to original first server display name and go back to first server // # Revert back to original first server display name and go back to first server
await ServerListScreen.getServerItemActive(newServerOneDisplayName).swipe('left'); await ServerListScreen.getServerItemActive(newServerOneDisplayName).swipe('left', 'slow');
await ServerListScreen.getServerItemEditOption(newServerOneDisplayName).tap(); await ServerListScreen.getServerItemEditOption(newServerOneDisplayName).tap();
await EditServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName); await EditServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName);
await EditServerScreen.saveButton.tap(); await EditServerScreen.saveButton.tap();
@ -229,20 +232,20 @@ describe('Server Login - Server List', () => {
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
await waitFor(ServerListScreen.getServerItemActive(serverOneDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC); await waitFor(ServerListScreen.getServerItemActive(serverOneDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left'); await ServerListScreen.getServerItemActive(serverOneDisplayName).swipe('left', 'slow');
await ServerListScreen.getServerItemRemoveOption(serverOneDisplayName).tap(); await ServerListScreen.getServerItemRemoveOption(serverOneDisplayName).tap();
// * Verify remove server alert is displayed // * Verify remove server alert is displayed
await expect(Alert.removeServerTitle(serverOneDisplayName)).toBeVisible(); await waitFor(Alert.removeServerTitle(serverOneDisplayName)).toBeVisible().withTimeout(timeouts.HALF_MIN);
// # Tap on remove button and go back to server list screen // # Tap on remove button and go back to server list screen
await Alert.removeButton3.tap(); await waitFor(Alert.removeButton1).toBeVisible().withTimeout(timeouts.HALF_MIN);
await Alert.removeButton1.tap();
await wait(timeouts.ONE_SEC); await wait(timeouts.ONE_SEC);
await ServerListScreen.open(); await ServerListScreen.open();
if (isIos()) { if (isIos()) {
await ServerListScreen.serverListTitle.swipe('up'); await ServerListScreen.serverListTitle.swipe('up');
} else if (isAndroid()) { } else if (isAndroid()) {
await waitFor(ServerListScreen.serverListTitle).toBeVisible().withTimeout(timeouts.TWO_SEC);
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
@ -252,7 +255,7 @@ describe('Server Login - Server List', () => {
// # Add first server back to the list and log in to the first server // # Add first server back to the list and log in to the first server
await ServerListScreen.addServerButton.tap(); await ServerListScreen.addServerButton.tap();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(); await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(serverOneUser); await LoginScreen.login(serverOneUser);
}); });
@ -270,17 +273,18 @@ describe('Server Login - Server List', () => {
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
await waitFor(ServerListScreen.getServerItemInactive(serverThreeDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC); await waitFor(ServerListScreen.getServerItemInactive(serverThreeDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ServerListScreen.getServerItemInactive(serverThreeDisplayName).swipe('left'); await ServerListScreen.getServerItemInactive(serverThreeDisplayName).swipe('left', 'slow');
await ServerListScreen.getServerItemLogoutOption(serverThreeDisplayName).tap(); await ServerListScreen.getServerItemLogoutOption(serverThreeDisplayName).tap();
// * Verify logout server alert is displayed // * Verify logout server alert is displayed
await expect(Alert.logoutTitle(serverThreeDisplayName)).toBeVisible(); await waitFor(Alert.logoutTitle(serverThreeDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC);
// # Tap on logout button // # Tap on logout button
await Alert.logoutButton3.tap(); await waitFor(Alert.logoutButton).toBeVisible().withTimeout(timeouts.TEN_SEC);
await Alert.logoutButton.tap();
// * Verify third server is logged out // * Verify third server is logged out
await ServerListScreen.getServerItemInactive(serverThreeDisplayName).swipe('left'); await ServerListScreen.getServerItemInactive(serverThreeDisplayName).swipe('left', 'slow');
await expect(ServerListScreen.getServerItemLoginOption(serverThreeDisplayName)).toBeVisible(); await expect(ServerListScreen.getServerItemLoginOption(serverThreeDisplayName)).toBeVisible();
// # Go back to first server // # Go back to first server
@ -301,7 +305,7 @@ describe('Server Login - Server List', () => {
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
await ServerListScreen.addServerButton.tap(); await ServerListScreen.addServerButton.tap();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(); await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.serverUrlInput.replaceText(serverTwoUrl); await ServerScreen.serverUrlInput.replaceText(serverTwoUrl);
if (isAndroid()) { if (isAndroid()) {
await ServerScreen.serverUrlInput.tapReturnKey(); await ServerScreen.serverUrlInput.tapReturnKey();
@ -346,9 +350,11 @@ describe('Server Login - Server List', () => {
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3); await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
} }
await waitFor(ServerListScreen.getServerItemInactive(serverTwoDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC); await waitFor(ServerListScreen.getServerItemInactive(serverTwoDisplayName)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left'); await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left', 'slow');
await ServerListScreen.getServerItemLogoutOption(serverTwoDisplayName).tap(); await ServerListScreen.getServerItemLogoutOption(serverTwoDisplayName).tap();
await Alert.logoutButton2.tap(); await wait(timeouts.FOUR_SEC);
await waitFor(Alert.logoutButton).toBeVisible().withTimeout(timeouts.HALF_MIN);
await Alert.logoutButton.tap();
await ServerListScreen.getServerItemActive(serverOneDisplayName).tap(); await ServerListScreen.getServerItemActive(serverOneDisplayName).tap();
}); });
}); });

View file

@ -1,23 +1,100 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
/* eslint-disable no-await-in-loop, no-console */
import {Plugin, System, User} from '@support/server_api'; import {Plugin, System, User} from '@support/server_api';
import {siteOneUrl} from '@support/test_config'; import {siteOneUrl} from '@support/test_config';
// Number of retry attempts
const MAX_RETRY_ATTEMPTS = 3;
// Delay between retries (in milliseconds)
const RETRY_DELAY = 5000;
let isFirstLaunch = true;
/**
* Launch the app with retry mechanism
* @returns {Promise<void>}
*/
export async function launchAppWithRetry(): Promise<void> {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
try {
if (isFirstLaunch) {
// For first launch, clean install
await device.launchApp({
newInstance: true,
delete: true,
permissions: {
notifications: 'YES',
camera: 'NO',
medialibrary: 'NO',
photos: 'NO',
},
launchArgs: {
detoxPrintBusyIdleResources: 'YES',
detoxDebugVisibility: 'YES',
detoxDisableSynchronization: 'YES',
detoxDisableHierarchyDump: 'YES',
reduceMotion: 'YES',
},
});
isFirstLaunch = false;
} else {
// For subsequent launches, reuse instance
await device.launchApp({
newInstance: false,
launchArgs: {
detoxPrintBusyIdleResources: 'YES',
detoxDebugVisibility: 'YES',
detoxDisableSynchronization: 'YES',
detoxURLBlacklistRegex: '.*localhost.*',
},
});
}
// Verify app is connected by executing a simple command
await device.reloadReactNative();
console.info(`✅ App launched successfully on attempt ${attempt}`);
return; // Success, exit the function
} 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) {
console.warn(`Waiting ${RETRY_DELAY}ms before retrying...`);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
// Force a new instance on retry
if (!isFirstLaunch && attempt > 1) {
console.warn('Forcing new instance for next attempt');
isFirstLaunch = true;
}
}
}
}
// 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}`);
}
beforeAll(async () => { beforeAll(async () => {
// Login as sysadmin and reset server configuration // Login as sysadmin and reset server configuration
await System.apiCheckSystemHealth(siteOneUrl); await System.apiCheckSystemHealth(siteOneUrl);
await User.apiAdminLogin(siteOneUrl); await User.apiAdminLogin(siteOneUrl);
await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl); await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl);
await launchAppWithRetry();
await device.launchApp({ });
newInstance: true,
launchArgs: {detoxPrintBusyIdleResources: 'YES'}, // Add this to speed up test cleanup
permissions: { afterAll(async () => {
notifications: 'YES', try {
camera: 'YES', await device.terminateApp();
medialibrary: 'YES', } catch (error) {
photos: 'YES', console.error('Error terminating app:', error);
}, }
});
}); });

View file

@ -66,8 +66,10 @@ describe('Smoke Test - Channels', () => {
const {channel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id}); const {channel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id});
await BrowseChannelsScreen.open(); await BrowseChannelsScreen.open();
await BrowseChannelsScreen.searchInput.replaceText(channel.name); await BrowseChannelsScreen.searchInput.replaceText(channel.name);
await wait(timeouts.ONE_SEC); await BrowseChannelsScreen.searchInput.tapReturnKey();
await wait(timeouts.FOUR_SEC);
await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2); await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2);
await ChannelScreen.closeScheduledMessageTooltip();
// * Verify on newly joined channel screen // * Verify on newly joined channel screen
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();
@ -90,8 +92,9 @@ describe('Smoke Test - Channels', () => {
// # Open create channel screen and create a new channel // # Open create channel screen and create a new channel
const displayName = `Channel ${getRandomId()}`; const displayName = `Channel ${getRandomId()}`;
await CreateOrEditChannelScreen.openCreateChannel(); await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.displayNameInput.replaceText(displayName); await CreateOrEditChannelScreen.displayNameInput.typeText(displayName);
await CreateOrEditChannelScreen.createButton.tap(); await wait(timeouts.FOUR_SEC);
await CreateOrEditChannelScreen.clickonCreateButton();
// * Verify on newly created public channel // * Verify on newly created public channel
await ChannelScreen.toBeVisible(); await ChannelScreen.toBeVisible();
@ -106,8 +109,10 @@ describe('Smoke Test - Channels', () => {
await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.open();
await CreateDirectMessageScreen.closeTutorial(); await CreateDirectMessageScreen.closeTutorial();
await CreateDirectMessageScreen.searchInput.replaceText(newUserDisplayName); await CreateDirectMessageScreen.searchInput.replaceText(newUserDisplayName);
await CreateDirectMessageScreen.searchInput.tapReturnKey();
await wait(timeouts.ONE_SEC); await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.getUserItem(newUser.id).tap(); await CreateDirectMessageScreen.getUserItem(newUser.id).tap();
await wait(timeouts.ONE_SEC);
await CreateDirectMessageScreen.startButton.tap(); await CreateDirectMessageScreen.startButton.tap();
// * Verify on direct message channel screen for the new user // * Verify on direct message channel screen for the new user
@ -134,11 +139,13 @@ describe('Smoke Test - Channels', () => {
await ChannelScreen.back(); await ChannelScreen.back();
}); });
it('MM-T4774_4 - should be able to find and edit a channel', async () => { // TODO: Uncomment the test when the issue is fixed https://mattermost.atlassian.net/browse/MM-63814
it.skip('MM-T4774_4 - should be able to find and edit a channel', async () => {
// # Open find channels screen, search for the channel to navigate to, and tap on the target channel item // # Open find channels screen, search for the channel to navigate to, and tap on the target channel item
await FindChannelsScreen.open(); await FindChannelsScreen.open();
await FindChannelsScreen.searchInput.replaceText(testChannel.name); await FindChannelsScreen.searchInput.replaceText(testChannel.name);
await wait(timeouts.ONE_SEC); await FindChannelsScreen.searchInput.tapReturnKey();
await waitFor(FindChannelsScreen.getFilteredChannelItem(testChannel.name)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await FindChannelsScreen.getFilteredChannelItem(testChannel.name).tap(); await FindChannelsScreen.getFilteredChannelItem(testChannel.name).tap();
// * Verify on target channel screen // * Verify on target channel screen

View file

@ -25,7 +25,7 @@ import {
ServerListScreen, ServerListScreen,
ServerScreen, ServerScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {isAndroid, isIos, timeouts} from '@support/utils'; import {isAndroid, isIos, timeouts, wait} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Smoke Test - Server Login', () => { describe('Smoke Test - Server Login', () => {
@ -69,7 +69,7 @@ describe('Smoke Test - Server Login', () => {
await User.apiAdminLogin(siteTwoUrl); await User.apiAdminLogin(siteTwoUrl);
const {user} = await Setup.apiInit(siteTwoUrl); const {user} = await Setup.apiInit(siteTwoUrl);
await ServerListScreen.addServerButton.tap(); await ServerListScreen.addServerButton.tap();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(); await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName); await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
await LoginScreen.login(user); await LoginScreen.login(user);
@ -102,6 +102,7 @@ describe('Smoke Test - Server Login', () => {
// # Tap on logout button // # Tap on logout button
await Alert.logoutButton.tap(); await Alert.logoutButton.tap();
await wait(timeouts.FOUR_SEC);
// * Verify second server is logged out // * Verify second server is logged out
await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left'); await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left');

View file

@ -73,9 +73,6 @@ describe('Teams - Invite', () => {
await expect(Invite.teamDisplayName).toHaveText(testTeam.display_name); await expect(Invite.teamDisplayName).toHaveText(testTeam.display_name);
await expect(Invite.teamIcon).toBeVisible(); await expect(Invite.teamIcon).toBeVisible();
// * Verify default Selection
await waitFor(Invite.screenSelection).toBeVisible().withTimeout(timeouts.FOUR_SEC);
// * Verify Server data // * Verify Server data
await expect(Invite.serverDisplayName).toHaveText(serverOneDisplayName); await expect(Invite.serverDisplayName).toHaveText(serverOneDisplayName);
@ -139,7 +136,8 @@ describe('Teams - Invite', () => {
await expect(Invite.getSearchListUserItem(testUser1.id)).not.toBeVisible(); await expect(Invite.getSearchListUserItem(testUser1.id)).not.toBeVisible();
// * Validate user is added to selected items // * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser1.id)).toBeVisible(); await expect(Invite.getSelectedChip()).toBeVisible();
await expect(Invite.getSelectedChip()).toHaveText(testUser1.username);
// # Send invitation // # Send invitation
await Invite.sendButton.tap(); await Invite.sendButton.tap();
@ -163,7 +161,8 @@ describe('Teams - Invite', () => {
await Invite.getSearchListUserItem(testUser2.id).tap(); await Invite.getSearchListUserItem(testUser2.id).tap();
// * Validate user is added to selected items // * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser2.id)).toBeVisible(); await expect(Invite.getSelectedChip()).toBeVisible();
await expect(Invite.getSelectedChip()).toHaveText(testUser2.username);
// # Send invitation // # Send invitation
await Invite.sendButton.tap(); await Invite.sendButton.tap();
@ -190,7 +189,8 @@ describe('Teams - Invite', () => {
await Invite.getSearchListUserItem(testUser3.id).tap(); await Invite.getSearchListUserItem(testUser3.id).tap();
// * Validate user is added to selected items // * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser3.id)).toBeVisible(); await expect(Invite.getSelectedChip()).toBeVisible();
await expect(Invite.getSelectedChip()).toHaveText(testUser3.username);
// # Search for a existent user already in team // # Search for a existent user already in team
await Invite.searchBarInput.replaceText(testUser.username); await Invite.searchBarInput.replaceText(testUser.username);
@ -201,9 +201,6 @@ describe('Teams - Invite', () => {
// # Select user item // # Select user item
await Invite.getSearchListUserItem(testUser.id).tap(); await Invite.getSearchListUserItem(testUser.id).tap();
// * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser.id)).toBeVisible();
// # Send invitation // # Send invitation
await Invite.sendButton.tap(); await Invite.sendButton.tap();

View file

@ -186,6 +186,8 @@ describe('Threads - Follow and Unfollow Thread', () => {
await wait(timeouts.ONE_SEC); await wait(timeouts.ONE_SEC);
await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).not.toBeVisible(); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).not.toBeVisible();
await wait(timeouts.FOUR_SEC);
// # Go back to channel list screen // # Go back to channel list screen
await GlobalThreadsScreen.back(); await GlobalThreadsScreen.back();
}); });

View file

@ -24,7 +24,7 @@ import {
ServerScreen, ServerScreen,
ThreadScreen, ThreadScreen,
} from '@support/ui/screen'; } from '@support/ui/screen';
import {getRandomId} from '@support/utils'; import {getRandomId, timeouts} from '@support/utils';
import {expect} from 'detox'; import {expect} from 'detox';
describe('Threads - Global Threads', () => { describe('Threads - Global Threads', () => {
@ -67,7 +67,7 @@ describe('Threads - Global Threads', () => {
await GlobalThreadsScreen.back(); await GlobalThreadsScreen.back();
}); });
it.skip('MM-T4805_2 - should be able to go to a thread a user started and followed -- UNSTABLE', async () => { it('MM-T4805_2 - should be able to go to a thread a user started and followed', async () => {
// # Create a thread started by the current user which current user replied to // # Create a thread started by the current user which current user replied to
const parentMessage = `Message ${getRandomId()}`; const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name); await ChannelScreen.open(channelsCategory, testChannel.name);
@ -85,7 +85,6 @@ describe('Threads - Global Threads', () => {
// # Go back to channel list screen, then go to global threads screen, and tap on all your threads button // # Go back to channel list screen, then go to global threads screen, and tap on all your threads button
await ThreadScreen.back(); await ThreadScreen.back();
await ChannelScreen.back(); await ChannelScreen.back();
await device.reloadReactNative();
await GlobalThreadsScreen.open(); await GlobalThreadsScreen.open();
await GlobalThreadsScreen.headerAllThreadsButton.tap(); await GlobalThreadsScreen.headerAllThreadsButton.tap();
@ -93,7 +92,13 @@ describe('Threads - Global Threads', () => {
await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).toBeVisible(); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).toBeVisible();
await expect(GlobalThreadsScreen.getThreadItemThreadStarterUserDisplayName(parentPost.id)).toHaveText(testUser.username); await expect(GlobalThreadsScreen.getThreadItemThreadStarterUserDisplayName(parentPost.id)).toHaveText(testUser.username);
await expect(GlobalThreadsScreen.getThreadItemThreadStarterChannelDisplayName(parentPost.id)).toHaveText(testChannel.display_name.toUpperCase()); await expect(GlobalThreadsScreen.getThreadItemThreadStarterChannelDisplayName(parentPost.id)).toHaveText(testChannel.display_name.toUpperCase());
await expect(GlobalThreadsScreen.getThreadItemFooterReplyCount(parentPost.id)).toHaveText('1 reply'); try {
// The reply count is shown as read.
await waitFor(GlobalThreadsScreen.getThreadItemFooterReplyCount(parentPost.id)).toBeVisible().withTimeout(timeouts.TEN_SEC);
} catch (error) {
// somtimes the app shows it as unread since the test actions are fast.
await waitFor(GlobalThreadsScreen.getThreadItemFooterUnreadReplies(parentPost.id)).toBeVisible().withTimeout(timeouts.TEN_SEC);
}
// # Tap on the thread // # Tap on the thread
await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); await GlobalThreadsScreen.getThreadItem(parentPost.id).tap();
@ -133,7 +138,7 @@ describe('Threads - Global Threads', () => {
await GlobalThreadsScreen.back(); await GlobalThreadsScreen.back();
}); });
it.skip('MM-T4805_4 - should be able to go to a thread a user replied to and followed -- UNSTABLE', async () => { it('MM-T4805_4 - should be able to go to a thread a user replied to and followed', async () => {
// # Create a thread started by another user which the current user replied to // # Create a thread started by another user which the current user replied to
const parentMessage = `Message ${getRandomId()}`; const parentMessage = `Message ${getRandomId()}`;
const {post: parentPost} = await Post.apiCreatePost(siteOneUrl, { const {post: parentPost} = await Post.apiCreatePost(siteOneUrl, {
@ -153,15 +158,13 @@ describe('Threads - Global Threads', () => {
// # Go back to channel list screen, then go to global threads screen, and tap on all your threads button // # Go back to channel list screen, then go to global threads screen, and tap on all your threads button
await ThreadScreen.back(); await ThreadScreen.back();
await ChannelScreen.back(); await ChannelScreen.back();
await device.reloadReactNative();
await GlobalThreadsScreen.open(); await GlobalThreadsScreen.open();
await GlobalThreadsScreen.headerAllThreadsButton.tap(); await GlobalThreadsScreen.headerAllThreadsButton.tap();
// * Verify the thread replied to by the current user is displayed // * Verify the thread replied to by the current user is displayed
await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).toBeVisible(); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).toBeVisible();
await expect(GlobalThreadsScreen.getThreadItemThreadStarterUserDisplayName(parentPost.id)).toHaveText('sysadmin'); await expect(GlobalThreadsScreen.getThreadItemThreadStarterUserDisplayName(parentPost.id)).toHaveText('admin');
await expect(GlobalThreadsScreen.getThreadItemThreadStarterChannelDisplayName(parentPost.id)).toHaveText(testChannel.display_name.toUpperCase()); await expect(GlobalThreadsScreen.getThreadItemThreadStarterChannelDisplayName(parentPost.id)).toHaveText(testChannel.display_name.toUpperCase());
await expect(GlobalThreadsScreen.getThreadItemFooterReplyCount(parentPost.id)).toHaveText('1 reply');
// # Tap on the thread // # Tap on the thread
await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); await GlobalThreadsScreen.getThreadItem(parentPost.id).tap();

View file

@ -119,9 +119,6 @@ describe('Threads - Save and Unsave Thread', () => {
// # Tap on thread overview unsave button // # Tap on thread overview unsave button
await ThreadScreen.getThreadOverviewUnsaveButton().tap(); await ThreadScreen.getThreadOverviewUnsaveButton().tap();
// * Verify saved text is not displayed on the post pre-header
await expect(postListPostItemPreHeaderText).not.toBeVisible();
// # Go back to channel list screen // # Go back to channel list screen
await ThreadScreen.back(); await ThreadScreen.back();
await ChannelScreen.back(); await ChannelScreen.back();

View file

@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
// Path to APNG4Android gif AndroidManifest.xml
const apngGifManifestPath = path.resolve(
__dirname,
'../node_modules/APNG4Android/gif/src/androidTest/AndroidManifest.xml',
);
// Content for APNG4Android gif AndroidManifest.xml
const apngGifManifestContent = `<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
android:exported="true"
tools:node="merge" />
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
android:exported="true"
tools:node="merge" />
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity"
android:exported="true"
tools:node="merge" />
</application>
</manifest>`;
// Path to APNG4Android frameanimation AndroidManifest.xml
const apngFrameAnimationManifestPath = path.resolve(
__dirname,
'../node_modules/APNG4Android/frameanimation/src/androidTest/AndroidManifest.xml',
);
// Content for APNG4Android frameanimation AndroidManifest.xml
const apngFrameAnimationManifestContent = `<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
android:exported="false"
tools:node="merge" />
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
android:exported="false"
tools:node="merge" />
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity"
android:exported="false"
tools:node="merge" />
</application>
</manifest>`;
// Function to create AndroidManifest.xml for APNG4Android gif
function createApngGifManifest() {
try {
fs.mkdirSync(path.dirname(apngGifManifestPath), {recursive: true});
fs.writeFileSync(apngGifManifestPath, apngGifManifestContent, 'utf-8');
console.log('APNG4Android gif AndroidManifest.xml created successfully.');
} catch (err) {
console.error(`Failed to create APNG4Android gif AndroidManifest.xml: ${err.message}`);
}
}
// Function to create AndroidManifest.xml for APNG4Android frameanimation
function createApngFrameAnimationManifest() {
try {
fs.mkdirSync(path.dirname(apngFrameAnimationManifestPath), {recursive: true});
fs.writeFileSync(apngFrameAnimationManifestPath, apngFrameAnimationManifestContent, 'utf-8');
console.log('APNG4Android frameanimation AndroidManifest.xml created successfully.');
} catch (err) {
console.error(`Failed to create APNG4Android frameanimation AndroidManifest.xml: ${err.message}`);
}
}
// Run updates
createApngGifManifest();
createApngFrameAnimationManifest();

415
detox/package-lock.json generated
View file

@ -23,7 +23,7 @@
"babel-plugin-module-resolver": "5.0.2", "babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3", "client-oauth2": "4.3.3",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"detox": "20.33.0", "detox": "20.37.0",
"form-data": "4.0.1", "form-data": "4.0.1",
"jest": "29.7.0", "jest": "29.7.0",
"jest-circus": "29.7.0", "jest-circus": "29.7.0",
@ -1098,14 +1098,13 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.26.3", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
"integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.26.3", "@babel/parser": "^7.27.0",
"@babel/types": "^7.26.3", "@babel/types": "^7.27.0",
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@ -1145,18 +1144,17 @@
} }
}, },
"node_modules/@babel/helper-create-class-features-plugin": { "node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.25.9", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
"integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-annotate-as-pure": "^7.25.9",
"@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9",
"@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9",
"@babel/helper-replace-supers": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
"@babel/traverse": "^7.25.9", "@babel/traverse": "^7.27.0",
"semver": "^6.3.1" "semver": "^6.3.1"
}, },
"engines": { "engines": {
@ -1261,11 +1259,10 @@
} }
}, },
"node_modules/@babel/helper-plugin-utils": { "node_modules/@babel/helper-plugin-utils": {
"version": "7.25.9", "version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
"integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1289,15 +1286,14 @@
} }
}, },
"node_modules/@babel/helper-replace-supers": { "node_modules/@babel/helper-replace-supers": {
"version": "7.25.9", "version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
"integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9",
"@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9",
"@babel/traverse": "^7.25.9" "@babel/traverse": "^7.26.5"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -1380,13 +1376,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.26.3", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.3" "@babel/types": "^7.27.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -2753,32 +2748,30 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.25.9", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.25.9", "@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.25.9", "@babel/parser": "^7.27.0",
"@babel/types": "^7.25.9" "@babel/types": "^7.27.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.26.4", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
"integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.26.2",
"@babel/generator": "^7.26.3", "@babel/generator": "^7.27.0",
"@babel/parser": "^7.26.3", "@babel/parser": "^7.27.0",
"@babel/template": "^7.25.9", "@babel/template": "^7.27.0",
"@babel/types": "^7.26.3", "@babel/types": "^7.27.0",
"debug": "^4.3.1", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
@ -2787,11 +2780,10 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.3", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9" "@babel/helper-validator-identifier": "^7.25.9"
@ -2807,6 +2799,26 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"dev": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dev": true,
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@flatten-js/interval-tree": { "node_modules/@flatten-js/interval-tree": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz",
@ -4276,6 +4288,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"dev": true
},
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@ -4300,6 +4318,37 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@wix-pilot/core": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@wix-pilot/core/-/core-3.2.6.tgz",
"integrity": "sha512-iGjHOOpO13ZNfP3CZDlYhBZ7R6ZqO+1R38nDv5BCIOecD4A4z5/WpEyapvw+ZFsECjFZLWhuKNV0KTJoQAvxYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.0",
"pngjs": "^7.0.0",
"winston": "^3.17.0"
},
"peerDependencies": {
"expect": "*"
},
"peerDependenciesMeta": {
"expect": {
"optional": true
}
}
},
"node_modules/@wix-pilot/detox": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@wix-pilot/detox/-/detox-1.0.11.tgz",
"integrity": "sha512-oCeYz7EhcWOyp0KbaJ3hipMbZ+nWuZ7Pvjno3sUzP1EebrZBZoRoDp16JZMR6H+i+h8OdqkrviI0mR2mYFZzMg==",
"dev": true,
"peerDependencies": {
"@wix-pilot/core": "^3.1.6",
"detox": ">=20.33.0",
"expect": "29.x.x || 28.x.x || ^27.2.5"
}
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -4932,18 +4981,6 @@
"pnpm": ">=8" "pnpm": ">=8"
} }
}, },
"node_modules/child-process-promise": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz",
"integrity": "sha512-Fi4aNdqBsr0mv+jgWxcZ/7rAIC2mgihrptyVI4foh/rrjY/3BNjfP9+oaiFx/fzim+1ZyCNBae0DlyfQhSugog==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6",
"node-version": "^1.0.0",
"promise-polyfill": "^6.0.1"
}
},
"node_modules/ci-info": { "node_modules/ci-info": {
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@ -5014,6 +5051,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5034,6 +5081,41 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dev": true,
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -5232,19 +5314,20 @@
} }
}, },
"node_modules/detox": { "node_modules/detox": {
"version": "20.33.0", "version": "20.37.0",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.33.0.tgz", "resolved": "https://registry.npmjs.org/detox/-/detox-20.37.0.tgz",
"integrity": "sha512-9KZC3NgCav/IvuWm/EaKNIL5lV2bJf23PPK67OHS2dMhJsDsNBRWP9k3RN7hzLzzUwAXMMoOmk+S1OpOsazNFA==", "integrity": "sha512-OUI2p3z31Yku1USBia0jrlwoQoa6rANvVWuUSJyjBlRRKDSeOf/DG/ivk+R5FO+lYZZtA3pIbl6LSql/Bf6DpQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@wix-pilot/core": "^3.2.2",
"@wix-pilot/detox": "^1.0.11",
"ajv": "^8.6.3", "ajv": "^8.6.3",
"bunyan": "^1.8.12", "bunyan": "^1.8.12",
"bunyan-debug-stream": "^3.1.0", "bunyan-debug-stream": "^3.1.0",
"caf": "^15.0.1", "caf": "^15.0.1",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"child-process-promise": "^2.2.0",
"detox-copilot": "^0.0.27",
"execa": "^5.1.1", "execa": "^5.1.1",
"find-up": "^5.0.0", "find-up": "^5.0.0",
"fs-extra": "^11.0.0", "fs-extra": "^11.0.0",
@ -5257,6 +5340,7 @@
"multi-sort-stream": "^1.0.3", "multi-sort-stream": "^1.0.3",
"multipipe": "^4.0.0", "multipipe": "^4.0.0",
"node-ipc": "9.2.1", "node-ipc": "9.2.1",
"promisify-child-process": "^4.1.2",
"proper-lockfile": "^3.0.2", "proper-lockfile": "^3.0.2",
"resolve-from": "^5.0.0", "resolve-from": "^5.0.0",
"sanitize-filename": "^1.6.1", "sanitize-filename": "^1.6.1",
@ -5290,13 +5374,6 @@
} }
} }
}, },
"node_modules/detox-copilot": {
"version": "0.0.27",
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.27.tgz",
"integrity": "sha512-H2febTNp0arVx2A8rvM1C2BwDiBEP/2Ya8Hd1mVyV66rR5u8om1gdIypaRGm+plpTLCHhlefe4+7qLtHgVzpng==",
"deprecated": "This package has been renamed to @wix-pilot/core. Please update your dependencies accordingly.",
"dev": true
},
"node_modules/detox/node_modules/find-up": { "node_modules/detox/node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -5508,6 +5585,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"dev": true
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -5723,6 +5806,12 @@
"bser": "2.1.1" "bser": "2.1.1"
} }
}, },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"dev": true
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -5793,6 +5882,12 @@
"flat": "cli.js" "flat": "cli.js"
} }
}, },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"dev": true
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.9", "version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -6159,8 +6254,7 @@
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true, "dev": true
"license": "ISC"
}, },
"node_modules/interpret": { "node_modules/interpret": {
"version": "1.4.0", "version": "1.4.0",
@ -7408,6 +7502,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"dev": true
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -7466,6 +7566,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"dev": true,
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -7789,16 +7906,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-version": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz",
"integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": { "node_modules/nopt": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
@ -7858,6 +7965,15 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dev": true,
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@ -8159,6 +8275,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/popper.js": { "node_modules/popper.js": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@ -8322,12 +8447,14 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/promise-polyfill": { "node_modules/promisify-child-process": {
"version": "6.1.0", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", "resolved": "https://registry.npmjs.org/promisify-child-process/-/promisify-child-process-4.1.2.tgz",
"integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", "integrity": "sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==",
"dev": true, "dev": true,
"license": "MIT" "engines": {
"node": ">=8"
}
}, },
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
@ -8816,6 +8943,15 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/sanitize-filename": { "node_modules/sanitize-filename": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
@ -8912,7 +9048,6 @@
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -8991,6 +9126,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dev": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"dev": true
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -9036,6 +9186,15 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/stack-utils": { "node_modules/stack-utils": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@ -9327,6 +9486,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"dev": true
},
"node_modules/throwback": { "node_modules/throwback": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz", "resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz",
@ -9400,6 +9565,15 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"dev": true,
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/truncate-utf8-bytes": { "node_modules/truncate-utf8-bytes": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@ -9692,6 +9866,70 @@
"which": "bin/which" "which": "bin/which"
} }
}, },
"node_modules/winston": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"dev": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.7.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.9.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"dev": true,
"dependencies": {
"logform": "^2.7.0",
"readable-stream": "^3.6.2",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/winston/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@ -9736,7 +9974,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8.3.0" "node": ">=8.3.0"
}, },

View file

@ -21,7 +21,7 @@
"babel-plugin-module-resolver": "5.0.2", "babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3", "client-oauth2": "4.3.3",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"detox": "20.33.0", "detox": "20.37.0",
"form-data": "4.0.1", "form-data": "4.0.1",
"jest": "29.7.0", "jest": "29.7.0",
"jest-circus": "29.7.0", "jest-circus": "29.7.0",
@ -42,13 +42,14 @@
"xml2js": "0.6.2" "xml2js": "0.6.2"
}, },
"scripts": { "scripts": {
"e2e:android-inject-settings": "node inject-detox-settings.js",
"e2e:android-create-emulator": "./create_android_emulator.sh", "e2e:android-create-emulator": "./create_android_emulator.sh",
"e2e:android-build": "detox build -c android.emu.debug", "e2e:android-build": "detox build -c android.emu.debug",
"e2e:android-test": "detox test -c android.emu.debug", "e2e:android-test": "detox test -c android.emu.debug --reuse --record-logs failing --take-screenshots failing",
"e2e:android-build-release": "detox build -c android.emu.release", "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: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-build": "IOS=true detox build -c ios.sim.debug",
"e2e:ios-test": "IOS=true detox test -c ios.sim.debug", "e2e:ios-test": "IOS=true detox test -c ios.sim.debug --reuse --record-logs failing --take-screenshots failing",
"e2e:ios-build-release": "detox build -c ios.sim.release", "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-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing",
"detox:config-gen": "cd utils && node generate_detox_config_ci.js", "detox:config-gen": "cd utils && node generate_detox_config_ci.js",

View file

@ -122,7 +122,7 @@ const saveReport = async () => {
// "ios-results-*" or "android-results-*" is the path in CI where the parallel detox jobs save the artifacts // "ios-results-*" or "android-results-*" is the path in CI where the parallel detox jobs save the artifacts
await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/${platform}-results*/jest-stare/${platform}-data*.json`]); await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/${platform}-results*/jest-stare/${platform}-data*.json`]);
generateJestStareHtmlReport(jestStareOutputDir, `${platform}-report.html`, jestStareCombinedFilePath); generateJestStareHtmlReport(jestStareOutputDir, `${platform}-report.html`, jestStareCombinedFilePath, platform);
if (process.env.CI) { if (process.env.CI) {
// Delete folders starting with "ios-results-" or "android-results-" only in CI environment // Delete folders starting with "ios-results-" or "android-results-" only in CI environment
@ -133,7 +133,7 @@ const saveReport = async () => {
} }
} }
} }
const result = await saveArtifacts(); const result = await saveArtifacts(platform);
if (result && result.success) { if (result && result.success) {
console.log('Successfully uploaded artifacts to S3:', result.reportLink); console.log('Successfully uploaded artifacts to S3:', result.reportLink);
} }

View file

@ -20,10 +20,8 @@ const {
DETOX_AWS_S3_BUCKET, DETOX_AWS_S3_BUCKET,
DETOX_AWS_ACCESS_KEY_ID, DETOX_AWS_ACCESS_KEY_ID,
DETOX_AWS_SECRET_ACCESS_KEY, DETOX_AWS_SECRET_ACCESS_KEY,
IOS,
REPORT_PATH, REPORT_PATH,
} = process.env; } = process.env;
const platform = IOS === 'true' ? 'ios' : 'android';
const s3 = new S3({ const s3 = new S3({
credentials: { credentials: {
@ -36,7 +34,7 @@ function getFiles(dirPath) {
return fs.existsSync(dirPath) ? readdir(dirPath) : []; return fs.existsSync(dirPath) ? readdir(dirPath) : [];
} }
async function saveArtifacts() { async function saveArtifacts(platform) {
if (!DETOX_AWS_S3_BUCKET || !DETOX_AWS_ACCESS_KEY_ID || !DETOX_AWS_SECRET_ACCESS_KEY) { if (!DETOX_AWS_S3_BUCKET || !DETOX_AWS_ACCESS_KEY_ID || !DETOX_AWS_SECRET_ACCESS_KEY) {
console.log('No AWS credentials found. Test artifacts not uploaded to S3.'); console.log('No AWS credentials found. Test artifacts not uploaded to S3.');

View file

@ -4,8 +4,8 @@
/* eslint-disable no-process-env */ /* eslint-disable no-process-env */
/* eslint-disable no-console */ /* eslint-disable no-console */
const fs = require('fs'); const fs = require('fs');
const deviceName = process.env.DEVICE_NAME || 'iPhone 16 Pro'; const deviceName = process.env.DEVICE_NAME || 'iPhone 15 Pro';
const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 18.1'; const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 17.2';
const detoxConfigTemplate = fs.readFileSync('../.detoxrc.json', 'utf8'); const detoxConfigTemplate = fs.readFileSync('../.detoxrc.json', 'utf8');
const detoxConfig = detoxConfigTemplate.replace('__DEVICE_NAME__', deviceName).replace('__DEVICE_OS_VERSION__', deviceOSVersion); const detoxConfig = detoxConfigTemplate.replace('__DEVICE_NAME__', deviceName).replace('__DEVICE_OS_VERSION__', deviceOSVersion);

View file

@ -129,9 +129,9 @@ function collectReportSuites(reports) {
}; };
} }
function generateJestStareHtmlReport(outputDir, outputFile, inputFilePath) { function generateJestStareHtmlReport(outputDir, outputFile, inputFilePath, platform) {
const suites = fse.readJsonSync(inputFilePath); const suites = fse.readJsonSync(inputFilePath);
processor(suites, {log: false, resultDir: outputDir, resultHtml: outputFile, reportHeadline: 'Mobile App E2E with Detox and Jest'}); processor(suites, {log: false, resultDir: outputDir, resultHtml: outputFile, reportHeadline: `${platform} Mobile App E2E with Detox and Jest`});
} }
async function mergeJestStareJsonFiles(outputFilePath, inputFiles) { async function mergeJestStareJsonFiles(outputFilePath, inputFiles) {

1
env.d.ts vendored
View file

@ -3,6 +3,7 @@
// So that typescript doesn't complain about importing `@env` through react-native-dotenv // So that typescript doesn't complain about importing `@env` through react-native-dotenv
declare module '@env' { declare module '@env' {
export const RUNNING_E2E: string;
} }
declare module 'react-native-dotenv' { declare module 'react-native-dotenv' {

View file

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {RUNNING_E2E} from '@env';
import TurboLogger from '@mattermost/react-native-turbo-log'; import TurboLogger from '@mattermost/react-native-turbo-log';
import {LogBox, Platform, UIManager} from 'react-native'; import {LogBox, Platform, UIManager} from 'react-native';
import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes'; import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
import {RUNNING_E2E} from 'react-native-dotenv';
import 'react-native-gesture-handler'; import 'react-native-gesture-handler';
import {Navigation} from 'react-native-navigation'; import {Navigation} from 'react-native-navigation';