diff --git a/.github/workflows/e2e-android-template.yml b/.github/workflows/e2e-android-template.yml index 1dbfdff1f..42d05c2ec 100644 --- a/.github/workflows/e2e-android-template.yml +++ b/.github/workflows/e2e-android-template.yml @@ -52,7 +52,7 @@ on: description: "Android API level" required: false type: string - default: "33" + default: "34" outputs: STATUS: value: ${{ jobs.generate-report.outputs.STATUS }} @@ -130,7 +130,7 @@ jobs: name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }} runs-on: ubuntu-latest-8-cores continue-on-error: true - timeout-minutes: 150 + timeout-minutes: 240 env: ANDROID_HOME: /usr/local/lib/android/sdk ANDROID_SDK_ROOT: /usr/local/lib/android/sdk @@ -170,6 +170,12 @@ jobs: cd detox npm install + - name: Set .env with RUNNING_E2E=true + run: | + cat > .env < 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: | - sdkmanager "system-images;android-34;default;x86_64" - sdkmanager "platform-tools" "emulator" - + yes | sdkmanager --install "platform-tools" "emulator" "platforms;android-34" "system-images;android-34;default;x86_64" "system-images;android-34;google_apis;x86_64" + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + - name: Create and run Android Emulator run: | cd detox 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 - name: Upload Android Test Report @@ -293,3 +310,36 @@ jobs: echo "" >> ${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} + + - 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 }} diff --git a/.github/workflows/e2e-detox-pr.yml b/.github/workflows/e2e-detox-pr.yml index 7b1881a8e..b8878dab8 100644 --- a/.github/workflows/e2e-detox-pr.yml +++ b/.github/workflows/e2e-detox-pr.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - main + - feature_schedule_posts types: - labeled @@ -104,6 +105,7 @@ jobs: cd detox npm install npm install -g detox-cli + npm run e2e:android-inject-settings npm run e2e:android-build - name: Upload Android Build diff --git a/.github/workflows/e2e-ios-template.yml b/.github/workflows/e2e-ios-template.yml index 547eb7b4e..260f79c06 100644 --- a/.github/workflows/e2e-ios-template.yml +++ b/.github/workflows/e2e-ios-template.yml @@ -38,12 +38,12 @@ on: description: "iPhone simulator name" required: false type: string - default: "iPhone 16 Pro" + default: "iPhone 15 Pro" ios_device_os_name: description: "iPhone simulator OS version" required: false type: string - default: "iOS 18.1" + default: "iOS 17.2" low_bandwidth_mode: description: "Enable low bandwidth mode" required: false @@ -81,7 +81,7 @@ env: WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} FAILURE_MESSAGE: "Something has failed" IOS: "true" - RUNNING_E2E: true + RUNNING_E2E: "true" jobs: generate-specs: @@ -124,7 +124,7 @@ jobs: name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }} runs-on: macos-14 continue-on-error: true - timeout-minutes: ${{ inputs.low_bandwidth_mode && 140 || 70 }} + timeout-minutes: ${{ inputs.low_bandwidth_mode && 240 || 180 }} env: IOS: true needs: @@ -153,7 +153,10 @@ jobs: path: mobile-artifacts - 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 id: prepare-low-bandwidth @@ -167,9 +170,6 @@ jobs: upload_speed: "3300" latency: "500" - - name: Start React Native Metro Server - run: npm run start & - - name: Install Detox Dependencies run: cd detox && npm i @@ -180,13 +180,25 @@ jobs: with: test_server_url: ${{ env.SITE_1_URL }} + - name: Set .env with RUNNING_E2E=true + run: | + cat > .env <> .env + - name: Run Detox E2E Tests continue-on-error: true # We want to run all the tests run: | + # Start the server + npm run start & + sleep 120 # Wait for watchman to finish querying the files cd detox + npm run clean-detox npm run detox:config-gen npm run e2e:ios-test -- ${{ matrix.specs }} env: + DETOX_DISABLE_HIERARCHY_DUMP: "YES" + DETOX_DISABLE_SCREENSHOT_TRACKING: "YES" + DETOX_LOGLEVEL: "debug" DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }} DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }} LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }} @@ -295,3 +307,36 @@ jobs: echo "" >> ${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} + + - 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 }} diff --git a/app/components/draft_scheduled_post_header/draft_scheduled_post_header.tsx b/app/components/draft_scheduled_post_header/draft_scheduled_post_header.tsx index 6d6fb8299..fc86242e0 100644 --- a/app/components/draft_scheduled_post_header/draft_scheduled_post_header.tsx +++ b/app/components/draft_scheduled_post_header/draft_scheduled_post_header.tsx @@ -137,7 +137,10 @@ const DraftAndScheduledPostHeader: React.FC = ({ return ( - + {isSent? intl.formatMessage({id: 'scheduled_post.header.sent', defaultMessage: 'Sent'}): intl.formatMessage( {id: 'channel_info.scheduled', defaultMessage: 'Send on {time}'}, {time: scheduledTime}, diff --git a/app/components/scheduled_post_indicator/index.tsx b/app/components/scheduled_post_indicator/index.tsx index 723d6f546..43b1fe829 100644 --- a/app/components/scheduled_post_indicator/index.tsx +++ b/app/components/scheduled_post_indicator/index.tsx @@ -88,7 +88,10 @@ function ScheduledPostIndicator({ name='clock-send-outline' size={18} /> - + {scheduledPostText} {' '} ``` ## iOS @@ -40,10 +58,30 @@ To build the iOS simulator for Detox, navigate to the `detox` folder and run: 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 To execute the Detox tests on iOS, navigate to the `detox` folder and run: ```sh npm run e2e:ios-test -``` \ No newline at end of file + +# 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. diff --git a/detox/create_android_emulator.sh b/detox/create_android_emulator.sh index 787c8d4d4..9888bcba7 100755 --- a/detox/create_android_emulator.sh +++ b/detox/create_android_emulator.sh @@ -28,7 +28,7 @@ create_avd() { local cpu_arch_family cpu_arch 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/" 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() { 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 emulator $emulator_opts -gpu host -accel on -qemu -m 4096 & @@ -82,6 +82,7 @@ start_server() { cd .. RUNNING_E2E=true npm run start & local timeout=120 interval=5 elapsed=0 + sleep $timeout until nc -z localhost 8081; do if [[ $elapsed -ge $timeout ]]; then diff --git a/detox/e2e/config.js b/detox/e2e/config.js index 574fb3a9a..c8b4f9fae 100644 --- a/detox/e2e/config.js +++ b/detox/e2e/config.js @@ -8,7 +8,7 @@ module.exports = { setupFilesAfterEnv: ['./test/setup.ts'], maxWorkers: 1, testSequencer: './custom_sequencer.js', - testTimeout: 120000, + testTimeout: 180000, rootDir: '.', testMatch: ['/test/**/*.e2e.ts'], transform: { diff --git a/detox/e2e/support/server_api/post.ts b/detox/e2e/support/server_api/post.ts index 70f9f0370..6583840e1 100644 --- a/detox/e2e/support/server_api/post.ts +++ b/detox/e2e/support/server_api/post.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {timeouts, wait} from '@support/utils'; + import client from './client'; 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 */ export const apiGetLastPostInChannel = async (baseUrl: string, channelId: string): Promise => { + await wait(timeouts.TWO_SEC); const {posts} = await apiGetPostsInChannel(baseUrl, channelId); return {post: posts[0]}; }; diff --git a/detox/e2e/support/ui/component/alert.ts b/detox/e2e/support/ui/component/alert.ts index a3f01b588..38393040f 100644 --- a/detox/e2e/support/ui/component/alert.ts +++ b/detox/e2e/support/ui/component/alert.ts @@ -41,6 +41,7 @@ class Alert { cancelButton = isAndroid() ? element(by.text('CANCEL')) : element(by.label('Cancel')).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); + deleteScheduledMessageButton = isAndroid() ? element(by.text('DELETE')) : element(by.label('Delete')).atIndex(1); 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); logoutButton2 = isAndroid() ? element(by.text('LOG OUT')) : element(by.label('Log out')).atIndex(2); diff --git a/detox/e2e/support/ui/screen/browse_channels.ts b/detox/e2e/support/ui/screen/browse_channels.ts index afa09fe75..a0031a50b 100644 --- a/detox/e2e/support/ui/screen/browse_channels.ts +++ b/detox/e2e/support/ui/screen/browse_channels.ts @@ -19,8 +19,10 @@ class BrowseChannelsScreen { channelDropdownTextArchived: 'browse_channels.channel_dropdown.text.archived', channelDropdownTextShared: 'browse_channels.channel_dropdown.text.shared', 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)); closeButton = element(by.id(this.testID.closeButton)); createButton = element(by.id(this.testID.createButton)); @@ -60,6 +62,15 @@ class BrowseChannelsScreen { await this.closeButton.tap(); 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(); diff --git a/detox/e2e/support/ui/screen/channel.ts b/detox/e2e/support/ui/screen/channel.ts index a9ded4777..6bfa21bcf 100644 --- a/detox/e2e/support/ui/screen/channel.ts +++ b/detox/e2e/support/ui/screen/channel.ts @@ -17,7 +17,7 @@ import { PostOptionsScreen, ThreadScreen, } from '@support/ui/screen'; -import {timeouts, wait} from '@support/utils'; +import {isIos, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; class ChannelScreen { @@ -47,8 +47,31 @@ class ChannelScreen { postPriorityUrgentMessage: 'post_priority_picker_item.urgent', postPriorityRequestAck: 'post_priority_picker_item.requested_ack.toggled.false.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)); postPriorityUrgentMessage = element(by.id(this.testID.postPriorityUrgentMessage)); postPriorityRequestAck = element(by.id(this.testID.postPriorityRequestAck)); @@ -132,7 +155,12 @@ class ChannelScreen { open = async (categoryKey: string, channelName: string) => { // # Open channel screen 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(); }; @@ -169,7 +197,7 @@ class ChannelScreen { await expect(postListPostItem).toBeVisible(); // # Open post options - await postListPostItem.longPress(); + await postListPostItem.longPress(timeouts.TWO_SEC); await PostOptionsScreen.toBeVisible(); await wait(timeouts.TWO_SEC); }; @@ -190,6 +218,12 @@ class ChannelScreen { await this.tapSendButton(); }; + enterMessageToSchedule = async (message: string) => { + await this.postInput.tap(); + await this.postInput.clearText(); + await this.postInput.replaceText(message); + }; + tapSendButton = async () => { // # Tap send button await this.sendButton.tap(); @@ -197,6 +231,11 @@ class ChannelScreen { await expect(this.sendButtonDisabled).toBeVisible(); }; + longPressSendButton = async () => { + // # Long press send button + await this.sendButton.longPress(); + }; + hasPostMessage = async (postId: string, postMessage: string) => { const {postListPostItem} = this.getPostListPostItem(postId, postMessage); await expect(postListPostItem).toBeVisible(); @@ -231,6 +270,47 @@ class ChannelScreen { applyPostPrioritySettings = async () => { 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} + */ + 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(); diff --git a/detox/e2e/support/ui/screen/channel_info.ts b/detox/e2e/support/ui/screen/channel_info.ts index a11abcde6..96dabe87e 100644 --- a/detox/e2e/support/ui/screen/channel_info.ts +++ b/detox/e2e/support/ui/screen/channel_info.ts @@ -116,6 +116,7 @@ class ChannelInfoScreen { noButton, yesButton, } = Alert; + await wait(timeouts.TWO_SEC); await expect(alertArchiveChannelTitle).toBeVisible(); await expect(noButton).toBeVisible(); await expect(yesButton).toBeVisible(); @@ -141,7 +142,7 @@ class ChannelInfoScreen { convertToPrivateChannel = async (channelDisplayName: string, {confirm = true} = {}) => { await this.scrollView.tap({x: 1, y: 1}); 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}); const { channelNowPrivateTitle, @@ -205,6 +206,7 @@ class ChannelInfoScreen { unarchiveChannel = async (alertUnarchiveChannelTitle: Detox.NativeElement, {confirm = true} = {}) => { 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}); const { noButton, diff --git a/detox/e2e/support/ui/screen/channel_list.ts b/detox/e2e/support/ui/screen/channel_list.ts index 8bf56bd8a..ea2b8008b 100644 --- a/detox/e2e/support/ui/screen/channel_list.ts +++ b/detox/e2e/support/ui/screen/channel_list.ts @@ -15,6 +15,7 @@ class ChannelListScreen { draftChannelInfo: 'draft_post.channel_info', draftbuttonListScreen: 'channel_list.drafts.button', draftCountListScreen: 'channel_list.drafts.count', + scheduledMessageCountListScreen: 'channel_list.scheduled_post.count', teamItemPrefix: 'team_sidebar.team_list.team_item.', channelListScreen: 'channel_list.screen', serverIcon: 'channel_list.servers.server_icon', diff --git a/detox/e2e/support/ui/screen/create_direct_message.ts b/detox/e2e/support/ui/screen/create_direct_message.ts index 56c68a613..5c6794a71 100644 --- a/detox/e2e/support/ui/screen/create_direct_message.ts +++ b/detox/e2e/support/ui/screen/create_direct_message.ts @@ -8,6 +8,7 @@ import {expect} from 'detox'; class CreateDirectMessageScreen { testID = { + selectedDMUserPrefix: 'create_direct_message.', selectedUserPrefix: 'create_direct_message.selected_user.', userItemPrefix: 'create_direct_message.user_list.user_item.', createDirectMessageScreen: 'create_direct_message.screen', @@ -20,8 +21,10 @@ class CreateDirectMessageScreen { sectionUserList: 'create_direct_message.user_list.section_list', tutorialHighlight: 'tutorial_highlight', tutorialSwipeLeft: 'tutorial_swipe_left', + scheduledPostTooltipCloseButton: 'scheduled_post.tooltip.close.button', }; + scheduledPostTooltipCloseButton = element(by.id(this.testID.scheduledPostTooltipCloseButton)); createDirectMessageScreen = element(by.id(this.testID.createDirectMessageScreen)); closeButton = element(by.id(this.testID.closeButton)); startButton = element(by.id(this.testID.startButton)); @@ -41,6 +44,10 @@ class CreateDirectMessageScreen { 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) => { return element(by.id(`${this.testID.selectedUserPrefix}${userId}.remove.button`)); }; @@ -70,7 +77,7 @@ class CreateDirectMessageScreen { await ChannelListScreen.headerPlusButton.tap(); await wait(timeouts.ONE_SEC); await ChannelListScreen.openDirectMessageItem.tap(); - + await wait(timeouts.TEN_SEC); return this.toBeVisible(); }; @@ -80,14 +87,19 @@ class CreateDirectMessageScreen { }; closeTutorial = async () => { - if (isIos()) { - await waitFor(this.tutorialHighlight).toExist().withTimeout(timeouts.TEN_SEC); - await this.tutorialSwipeLeft.tap(); - await expect(this.tutorialHighlight).not.toExist(); - } else { - await wait(timeouts.ONE_SEC); - await device.pressBack(); - await wait(timeouts.ONE_SEC); + try { + if (isIos()) { + await waitFor(this.tutorialHighlight).toExist().withTimeout(timeouts.HALF_MIN); + await this.tutorialSwipeLeft.tap(); + await expect(this.tutorialHighlight).not.toExist(); + } else { + 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:'); } }; } diff --git a/detox/e2e/support/ui/screen/create_or_edit_channel.ts b/detox/e2e/support/ui/screen/create_or_edit_channel.ts index 98e1877c8..e3110a549 100644 --- a/detox/e2e/support/ui/screen/create_or_edit_channel.ts +++ b/detox/e2e/support/ui/screen/create_or_edit_channel.ts @@ -60,7 +60,7 @@ class CreateOrEditChannelScreen { openEditChannel = async () => { // # Open edit channel screen await ChannelInfoScreen.scrollView.tap({x: 1, y: 1}); - await ChannelInfoScreen.scrollView.scroll(100, 'down'); + await ChannelInfoScreen.scrollView.scrollTo('bottom'); await ChannelInfoScreen.editChannelOption.tap(); return this.toBeVisible(); @@ -96,6 +96,16 @@ class CreateOrEditChannelScreen { await this.makePrivateToggledOn.tap(); 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(); diff --git a/detox/e2e/support/ui/screen/draft_screen.ts b/detox/e2e/support/ui/screen/draft_screen.ts index 75fc591d3..4c08d23cd 100644 --- a/detox/e2e/support/ui/screen/draft_screen.ts +++ b/detox/e2e/support/ui/screen/draft_screen.ts @@ -12,7 +12,7 @@ class DraftScreen { draftMessageContent: 'draft_message', draftScreen: 'global_drafts_list', draftTooltipCloseButton: 'draft.tooltip.close.button', - draftPost: 'draft_post', + draftPost: 'draft_message', draftSendButton: 'send_draft_button', draftEmptyTitle: 'drafts.empty.title', requestACKIcon: 'drafts.requested_ack.icon', diff --git a/detox/e2e/support/ui/screen/edit_profile.ts b/detox/e2e/support/ui/screen/edit_profile.ts index 088bd4013..b99d8fa20 100644 --- a/detox/e2e/support/ui/screen/edit_profile.ts +++ b/detox/e2e/support/ui/screen/edit_profile.ts @@ -13,10 +13,10 @@ class EditProfileScreen { closeButton: 'close.edit_profile.button', saveButton: 'edit_profile.save.button', scrollView: 'edit_profile.scroll_view', - firstNameInput: 'edit_profile_form.first_name.input', - firstNameInputDisabled: 'edit_profile_form.first_name.input.disabled', - lastNameInput: 'edit_profile_form.last_name.input', - lastNameInputDisabled: 'edit_profile_form.last_name.input.disabled', + firstNameInput: 'edit_profile_form.firstName.input', + firstNameInputDisabled: 'edit_profile_form.firstName.input.disabled', + lastNameInput: 'edit_profile_form.lastName.input', + lastNameInputDisabled: 'edit_profile_form.lastName.input.disabled', usernameInput: 'edit_profile_form.username.input', usernameInputDisabled: 'edit_profile_form.username.input.disabled', emailInput: 'edit_profile_form.email.input', diff --git a/detox/e2e/support/ui/screen/index.ts b/detox/e2e/support/ui/screen/index.ts index 1abe58da3..c2c0ba4bc 100644 --- a/detox/e2e/support/ui/screen/index.ts +++ b/detox/e2e/support/ui/screen/index.ts @@ -35,6 +35,7 @@ import PushNotificationSettingsScreen from './push_notification_settings'; import ReactionsScreen from './reactions'; import RecentMentionsScreen from './recent_mentions'; import SavedMessagesScreen from './saved_messages'; +import ScheduleMessageScreen from './scheduled_message_screen'; import SearchMessagesScreen from './search_messages'; import SelectTimezoneScreen from './select_timezone'; import ServerScreen from './server'; @@ -95,4 +96,5 @@ export { TimezoneDisplaySettingsScreen, UserProfileScreen, DraftScreen, + ScheduleMessageScreen, }; diff --git a/detox/e2e/support/ui/screen/invite.ts b/detox/e2e/support/ui/screen/invite.ts index 7c3c6c90b..aaf13f0b0 100644 --- a/detox/e2e/support/ui/screen/invite.ts +++ b/detox/e2e/support/ui/screen/invite.ts @@ -79,6 +79,10 @@ class InviteScreen { return element(by.id(`${this.testID.selectedItemPrefix}.${id}`)); }; + getSelectedChip = () => { + return element(by.id('invite.selected_item.display_name')); + }; + getSummaryReportSent = () => { return element(by.id(`${this.testID.summaryReportPrefix}.sent`)); }; diff --git a/detox/e2e/support/ui/screen/login.ts b/detox/e2e/support/ui/screen/login.ts index 4a5ff3ccb..086acbdc1 100644 --- a/detox/e2e/support/ui/screen/login.ts +++ b/detox/e2e/support/ui/screen/login.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. 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'; class LoginScreen { @@ -21,8 +21,10 @@ class LoginScreen { forgotPasswordButton: 'login_form.forgot_password.button', signinButton: 'login_form.signin.button', 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)); backButton = element(by.id(this.testID.backButton)); titleLoginToAccount = element(by.id(this.testID.titleLoginToAccount)); @@ -58,17 +60,20 @@ class LoginScreen { await expect(this.loginScreen).not.toBeVisible(); }; - login = async (user: any = {}) => { + loginWithRetryIfStuck = async (user: any = {}) => { await this.toBeVisible(); - await this.usernameInput.tap({x: 150, y: 10}); await this.usernameInput.replaceText(user.newUser.email); await this.passwordInput.tap(); 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 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 = {}) => { @@ -78,9 +83,9 @@ class LoginScreen { await this.usernameInput.replaceText(user.username); await this.passwordInput.tap(); await this.passwordInput.replaceText(user.password); + await this.loginFormInfoText.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); }; } diff --git a/detox/e2e/support/ui/screen/scheduled_message_screen.ts b/detox/e2e/support/ui/screen/scheduled_message_screen.ts new file mode 100644 index 000000000..c76d8e4a0 --- /dev/null +++ b/detox/e2e/support/ui/screen/scheduled_message_screen.ts @@ -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 => { + 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; diff --git a/detox/e2e/support/ui/screen/search_messages.ts b/detox/e2e/support/ui/screen/search_messages.ts index aa5b92868..b0e3ecbb3 100644 --- a/detox/e2e/support/ui/screen/search_messages.ts +++ b/detox/e2e/support/ui/screen/search_messages.ts @@ -68,7 +68,7 @@ class SearchMessagesScreen { }; 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 () => { @@ -87,6 +87,7 @@ class SearchMessagesScreen { openPostOptionsFor = async (postId: string, text: string) => { const {postListPostItem} = this.getPostListPostItem(postId, text); + await wait(timeouts.TWO_SEC); await expect(postListPostItem).toBeVisible(); // # Open post options diff --git a/detox/e2e/support/ui/screen/server.ts b/detox/e2e/support/ui/screen/server.ts index 360994eb8..8ad390f7d 100644 --- a/detox/e2e/support/ui/screen/server.ts +++ b/detox/e2e/support/ui/screen/server.ts @@ -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 () => { diff --git a/detox/e2e/support/ui/screen/thread.ts b/detox/e2e/support/ui/screen/thread.ts index b6e57ab47..762e742d2 100644 --- a/detox/e2e/support/ui/screen/thread.ts +++ b/detox/e2e/support/ui/screen/thread.ts @@ -92,7 +92,7 @@ class ThreadScreen { back = async () => { 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) => { @@ -112,6 +112,17 @@ class ThreadScreen { 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 () => { // # Tap send button await this.sendButton.tap(); diff --git a/detox/e2e/support/utils/index.ts b/detox/e2e/support/utils/index.ts index 6612b8d81..6f533bc7f 100644 --- a/detox/e2e/support/utils/index.ts +++ b/detox/e2e/support/utils/index.ts @@ -77,3 +77,28 @@ export const timeouts = { FOUR_MIN: MINUTE * 4, }; +/** + * Retry a function with reload + * @param {function} func - function to retry + * @param {number} retries - number of retries + * @return {Promise} - 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, retries: number = 2): Promise { + 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; + } + } + } +} diff --git a/detox/e2e/test/account/edit_profile.e2e.ts b/detox/e2e/test/account/edit_profile.e2e.ts index e566e69de..2318ca021 100644 --- a/detox/e2e/test/account/edit_profile.e2e.ts +++ b/detox/e2e/test/account/edit_profile.e2e.ts @@ -54,6 +54,7 @@ describe('Account - Edit Profile', () => { await expect(EditProfileScreen.closeButton).toBeVisible(); await expect(EditProfileScreen.saveButton).toBeVisible(); await expect(EditProfileScreen.getEditProfilePicture(testUser.id)).toBeVisible(); + await waitFor(EditProfileScreen.firstNameInput).toExist().withTimeout(2000); if (isIos()) { await expect(EditProfileScreen.firstNameInput).toHaveValue(testUser.first_name); await expect(EditProfileScreen.lastNameInput).toHaveValue(testUser.last_name); diff --git a/detox/e2e/test/account/mention_notification_settings.e2e.ts b/detox/e2e/test/account/mention_notification_settings.e2e.ts index f5e5440b3..bdaec347b 100644 --- a/detox/e2e/test/account/mention_notification_settings.e2e.ts +++ b/detox/e2e/test/account/mention_notification_settings.e2e.ts @@ -21,7 +21,7 @@ import { ServerScreen, SettingsScreen, } from '@support/ui/screen'; -import {getRandomId, isIos} from '@support/utils'; +import {getRandomId, isIos, timeouts} from '@support/utils'; import {expect} from 'detox'; describe('Account - Settings - Mention Notification Settings', () => { @@ -84,7 +84,7 @@ describe('Account - Settings - Mention Notification Settings', () => { const triggerMentionKeyword = await MentionNotificationSettingsScreen.getKeywordTriggerElement(keywords); await expect(triggerMentionKeyword).toHaveText(keywords.replace(/ /g, '').toLowerCase()); } 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 diff --git a/detox/e2e/test/account/theme_display_settings.e2e.ts b/detox/e2e/test/account/theme_display_settings.e2e.ts index ad3ce4b88..466eeb5b1 100644 --- a/detox/e2e/test/account/theme_display_settings.e2e.ts +++ b/detox/e2e/test/account/theme_display_settings.e2e.ts @@ -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 () => { // # Tap on a sapphire option and tap on back button await ThemeDisplaySettingsScreen.sapphireOption.tap(); - await ThemeDisplaySettingsScreen.back(); // * Verify on display settings screen and sapphire is set await DisplaySettingsScreen.toBeVisible(); @@ -80,7 +79,6 @@ describe('Account - Settings - Theme Display Settings', () => { // # Tap on denim option and tap on back button await ThemeDisplaySettingsScreen.denimOption.tap(); - await ThemeDisplaySettingsScreen.back(); // * Verify on display settings screen and denim is set await DisplaySettingsScreen.toBeVisible(); diff --git a/detox/e2e/test/autocomplete/at_mention.e2e.ts b/detox/e2e/test/autocomplete/at_mention.e2e.ts index 874a4edc3..f9edeea9f 100644 --- a/detox/e2e/test/autocomplete/at_mention.e2e.ts +++ b/detox/e2e/test/autocomplete/at_mention.e2e.ts @@ -52,7 +52,7 @@ describe('Autocomplete - At-Mention', () => { // # Log in to server await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); - await LoginScreen.login(testUser); + await LoginScreen.login(user); // * Verify on channel list screen await ChannelListScreen.toBeVisible(); diff --git a/detox/e2e/test/autocomplete/slash_suggestion.e2e.ts b/detox/e2e/test/autocomplete/slash_suggestion.e2e.ts index b550650f5..f765cfb77 100644 --- a/detox/e2e/test/autocomplete/slash_suggestion.e2e.ts +++ b/detox/e2e/test/autocomplete/slash_suggestion.e2e.ts @@ -154,7 +154,7 @@ describe('Autocomplete - Slash Suggestion', () => { await slashSuggestionAutocomplete.tap(); // * 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 await ChannelScreen.postInput.typeText('/'); diff --git a/detox/e2e/test/channels/browse_channels.e2e.ts b/detox/e2e/test/channels/browse_channels.e2e.ts index dc1ce5e7a..6af5c8244 100644 --- a/detox/e2e/test/channels/browse_channels.e2e.ts +++ b/detox/e2e/test/channels/browse_channels.e2e.ts @@ -85,6 +85,7 @@ describe('Channels - Browse Channels', () => { // # Tap on the new public channel item await BrowseChannelsScreen.getChannelItem(channel.name).multiTap(2); + await BrowseChannelsScreen.dismissScheduledPostTooltip(); // * Verify on newly joined public channel screen await ChannelScreen.toBeVisible(); diff --git a/detox/e2e/test/channels/channel_info.e2e.ts b/detox/e2e/test/channels/channel_info.e2e.ts index b2ee61870..2ec1b8700 100644 --- a/detox/e2e/test/channels/channel_info.e2e.ts +++ b/detox/e2e/test/channels/channel_info.e2e.ts @@ -61,6 +61,7 @@ describe('Channels - Channel Info', () => { await expect(ChannelInfoScreen.muteAction).toBeVisible(); await expect(ChannelInfoScreen.joinStartCallAction).toBeVisible(); await expect(ChannelInfoScreen.ignoreMentionsOptionToggledOff).toBeVisible(); + await ChannelInfoScreen.scrollView.scrollTo('bottom'); await expect(ChannelInfoScreen.pinnedMessagesOption).toBeVisible(); await expect(ChannelInfoScreen.copyChannelLinkOption).toBeVisible(); await ChannelInfoScreen.scrollView.scrollTo('bottom'); diff --git a/detox/e2e/test/channels/channel_list.e2e.ts b/detox/e2e/test/channels/channel_list.e2e.ts index 58c03164f..a684a73f6 100644 --- a/detox/e2e/test/channels/channel_list.e2e.ts +++ b/detox/e2e/test/channels/channel_list.e2e.ts @@ -82,6 +82,7 @@ describe('Channels - Channel List', () => { it('MM-T4728_2 - should be able to switch between channels', async () => { // # Tap on a first channel await ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name).tap(); + await ChannelScreen.closeScheduledMessageTooltip(); // * Verify on first channel await ChannelScreen.toBeVisible(); diff --git a/detox/e2e/test/channels/convert_to_private_channel.e2e.ts b/detox/e2e/test/channels/convert_to_private_channel.e2e.ts index cacee2eee..10899e09b 100644 --- a/detox/e2e/test/channels/convert_to_private_channel.e2e.ts +++ b/detox/e2e/test/channels/convert_to_private_channel.e2e.ts @@ -17,16 +17,18 @@ import { ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; -import {getAdminAccount, getRandomId, timeouts} from '@support/utils'; +import {getAdminAccount, getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Convert to Private Channel', () => { const siteOneDisplayName = 'Server 1'; beforeAll(async () => { + // # Log in to server as admin await ServerScreen.connectToServer(siteOneUrl, siteOneDisplayName); await LoginScreen.loginAsAdmin(getAdminAccount()); + await wait(timeouts.TWO_SEC); }); beforeEach(async () => { @@ -45,6 +47,8 @@ describe('Channels - Convert to Private Channel', () => { await CreateOrEditChannelScreen.openCreateChannel(); await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName); await CreateOrEditChannelScreen.createButton.tap(); + await wait(timeouts.TWO_SEC); + await ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount.tap(); await ChannelInfoScreen.open(); await ChannelInfoScreen.convertToPrivateChannel(channelDisplayName, {confirm: true}); diff --git a/detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts b/detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts index 9e2269eae..331489971 100644 --- a/detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts +++ b/detox/e2e/test/channels/create_channel_and_edit_channel_header.e2e.ts @@ -20,7 +20,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; -import {getRandomId, isIos} from '@support/utils'; +import {getRandomId, isIos, timeouts} from '@support/utils'; import {expect} from 'detox'; 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.headerInput.replaceText(header); await CreateOrEditChannelScreen.createButton.tap(); + await waitFor(ChannelScreen.scheduledPostTooltipCloseButton).toBeVisible().withTimeout(timeouts.FOUR_SEC); + await ChannelScreen.scheduledPostTooltipCloseButton.tap(); // * Verify on newly created public channel await ChannelScreen.toBeVisible(); diff --git a/detox/e2e/test/channels/create_direct_message.e2e.ts b/detox/e2e/test/channels/create_direct_message.e2e.ts index 9bdfdfe35..83d9d401a 100644 --- a/detox/e2e/test/channels/create_direct_message.e2e.ts +++ b/detox/e2e/test/channels/create_direct_message.e2e.ts @@ -25,6 +25,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Create Direct Message', () => { @@ -87,10 +88,12 @@ describe('Channels - Create Direct Message', () => { await CreateDirectMessageScreen.getUserItem(newUser.id).tap(); // * Verify the new user is selected - await expect(CreateDirectMessageScreen.getSelectedUserDisplayName(newUser.id)).toBeVisible(); + await expect(CreateDirectMessageScreen.getSelectedDMUserDisplayName(newUser.id)).toBeVisible(); // # Tap on start button 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 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 await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.searchInput.replaceText(firstNewUser.username); + await CreateDirectMessageScreen.searchInput.tapReturnKey(); + await wait(timeouts.ONE_SEC); await CreateDirectMessageScreen.getUserItem(firstNewUser.id).tap(); // * 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 await CreateDirectMessageScreen.searchInput.replaceText(secondNewUser.username); + await CreateDirectMessageScreen.searchInput.tapReturnKey(); + await wait(timeouts.ONE_SEC); await CreateDirectMessageScreen.getUserItem(secondNewUser.id).tap(); // * 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 await CreateDirectMessageScreen.startButton.tap(); @@ -154,6 +161,8 @@ describe('Channels - Create Direct Message', () => { const searchTerm = 'blahblahblahblah'; await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.searchInput.replaceText(searchTerm); + await CreateDirectMessageScreen.searchInput.tapReturnKey(); + await wait(timeouts.ONE_SEC); // * Verify empty search state for create direct message await expect(element(by.text(`No matches found for “${searchTerm}”`))).toBeVisible(); diff --git a/detox/e2e/test/channels/edit_channel.e2e.ts b/detox/e2e/test/channels/edit_channel.e2e.ts index 7c6e12a30..9a3878ced 100644 --- a/detox/e2e/test/channels/edit_channel.e2e.ts +++ b/detox/e2e/test/channels/edit_channel.e2e.ts @@ -26,7 +26,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; -import {isIos} from '@support/utils'; +import {isAndroid, isIos, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Edit Channel', () => { @@ -112,6 +112,10 @@ describe('Channels - Edit Channel', () => { await ChannelInfoScreen.toBeVisible(); await expect(ChannelInfoScreen.publicPrivateTitleDisplayName).toHaveText(`${testChannel.display_name} name`); 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(); // # Go back to channel screen @@ -154,8 +158,11 @@ describe('Channels - Edit Channel', () => { await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser1.username); await CreateDirectMessageScreen.getUserItem(testOtherUser1.id).tap(); await CreateDirectMessageScreen.searchInput.replaceText(testOtherUser2.username); + await CreateDirectMessageScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); await CreateDirectMessageScreen.getUserItem(testOtherUser2.id).tap(); await CreateDirectMessageScreen.startButton.tap(); + await wait(timeouts.TWO_SEC); // * Verify on group message channel screen await ChannelScreen.toBeVisible(); diff --git a/detox/e2e/test/channels/find_channels.e2e.ts b/detox/e2e/test/channels/find_channels.e2e.ts index 723176515..013590455 100644 --- a/detox/e2e/test/channels/find_channels.e2e.ts +++ b/detox/e2e/test/channels/find_channels.e2e.ts @@ -25,7 +25,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; -import {timeouts, wait} from '@support/utils'; +import {isIos, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Find Channels', () => { @@ -68,7 +68,8 @@ describe('Channels - Find Channels', () => { 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 await FindChannelsScreen.open(); await FindChannelsScreen.searchInput.replaceText(testChannel.display_name); @@ -98,7 +99,7 @@ describe('Channels - Find Channels', () => { 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 const {user: testOtherUser1} = await User.apiCreateUser(siteOneUrl, {prefix: 'a'}); await Team.apiAddUserToTeam(siteOneUrl, testOtherUser1.id, testTeam.id); @@ -127,7 +128,7 @@ describe('Channels - Find Channels', () => { 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 const {channel: archivedChannel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id}); await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, archivedChannel.id); @@ -171,6 +172,12 @@ describe('Channels - Find Channels', () => { }); 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 expect(ChannelScreen.headerTitle).toHaveText(display_name); await expect(ChannelScreen.introDisplayName).toHaveText(display_name); diff --git a/detox/e2e/test/channels/unarchive_channel.e2e.ts b/detox/e2e/test/channels/unarchive_channel.e2e.ts index dbfe12d31..bbfc48292 100644 --- a/detox/e2e/test/channels/unarchive_channel.e2e.ts +++ b/detox/e2e/test/channels/unarchive_channel.e2e.ts @@ -17,16 +17,18 @@ import { ServerScreen, ChannelInfoScreen, } from '@support/ui/screen'; -import {getAdminAccount, getRandomId, timeouts} from '@support/utils'; +import {getAdminAccount, getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Channels - Unarchive Channel', () => { const serverOneDisplayName = 'Server 1'; beforeAll(async () => { + // # Log in to server as admin await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); await LoginScreen.loginAsAdmin(getAdminAccount()); + await wait(timeouts.TWO_SEC); }); beforeEach(async () => { @@ -45,6 +47,9 @@ describe('Channels - Unarchive Channel', () => { await CreateOrEditChannelScreen.openCreateChannel(); await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName); await CreateOrEditChannelScreen.createButton.tap(); + await wait(timeouts.TWO_SEC); + await expect(ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount).toBeVisible(); + await ChannelScreen.scheduledPostTooltipCloseButtonAdminAccount.tap(); await ChannelInfoScreen.open(); await ChannelInfoScreen.archivePublicChannel({confirm: true}); diff --git a/detox/e2e/test/messaging/at_mention.e2e.ts b/detox/e2e/test/messaging/at_mention.e2e.ts index e5af7efae..dba1d2a57 100644 --- a/detox/e2e/test/messaging/at_mention.e2e.ts +++ b/detox/e2e/test/messaging/at_mention.e2e.ts @@ -85,7 +85,7 @@ describe('Messaging - At-Mention', () => { 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 [...Array(3).keys()].forEach(async (key) => { const {user} = await User.apiCreateUser(siteOneUrl, {prefix: `a-${key}-`}); diff --git a/detox/e2e/test/messaging/emojis_and_reactions.e2e.ts b/detox/e2e/test/messaging/emojis_and_reactions.e2e.ts index 4a1615e4f..f08f0d836 100644 --- a/detox/e2e/test/messaging/emojis_and_reactions.e2e.ts +++ b/detox/e2e/test/messaging/emojis_and_reactions.e2e.ts @@ -152,7 +152,7 @@ describe('Messaging - Emojis and Reactions', () => { await EmojiPickerScreen.open(); // * 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(); @@ -175,5 +175,9 @@ describe('Messaging - Emojis and Reactions', () => { // * Verify empty search state for emoji picker await expect(element(by.text(`No matches found for “${searchTerm}”`))).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(); }); }); diff --git a/detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts b/detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts index 266e12cec..be314d622 100644 --- a/detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts +++ b/detox/e2e/test/messaging/follow_and_unfollow_messsage.e2e.ts @@ -23,7 +23,7 @@ import { PostOptionsScreen, ServerScreen, } from '@support/ui/screen'; -import {getRandomId, timeouts} from '@support/utils'; +import {getRandomId, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Messaging - Follow and Unfollow Message', () => { @@ -62,19 +62,21 @@ describe('Messaging - Follow and Unfollow Message', () => { await expect(postListPostItem).toBeVisible(); // # 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(); // * Verify message is followed by user via post footer 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 await ChannelScreen.openPostOptionsFor(post.id, message); await PostOptionsScreen.followingThreadOption.tap(); // * 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 await ChannelScreen.back(); diff --git a/detox/e2e/test/messaging/markdown_latex.e2e.ts b/detox/e2e/test/messaging/markdown_latex.e2e.ts index 14cb5eeb1..cec26a5c9 100644 --- a/detox/e2e/test/messaging/markdown_latex.e2e.ts +++ b/detox/e2e/test/messaging/markdown_latex.e2e.ts @@ -48,7 +48,8 @@ describe('Messaging - Markdown Latex', () => { 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 // 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]'; diff --git a/detox/e2e/test/messaging/markdown_list.e2e.ts b/detox/e2e/test/messaging/markdown_list.e2e.ts index e5e3297cb..13ca17e5a 100644 --- a/detox/e2e/test/messaging/markdown_list.e2e.ts +++ b/detox/e2e/test/messaging/markdown_list.e2e.ts @@ -22,6 +22,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {isAndroid, isIos} from '@support/utils'; import {expect} from 'detox'; describe('Messaging - Markdown List', () => { @@ -48,7 +49,7 @@ describe('Messaging - Markdown List', () => { 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 const item1 = 'item one'; const item2 = 'item two'; @@ -61,15 +62,24 @@ describe('Messaging - Markdown List', () => { const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); const {postListPostItemListItem, postListPostItemListItemBullet} = ChannelScreen.getPostListPostItem(post.id); await expect(postListPostItemListItem.atIndex(0)).toBeVisible(); - await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('•'); 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(postListPostItemListItem.atIndex(2)).toBeVisible(); - await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('◦'); 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 await ChannelScreen.back(); }); @@ -89,13 +99,20 @@ describe('Messaging - Markdown List', () => { await expect(postListPostItemListItem.atIndex(0)).toBeVisible(); await expect(postListPostItemListItemBullet.atIndex(0)).toHaveText('1.'); 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(postListPostItemListItem.atIndex(2)).toBeVisible(); - await expect(postListPostItemListItemBullet.atIndex(2)).toHaveText('3.'); 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 await ChannelScreen.back(); }); diff --git a/detox/e2e/test/messaging/markdown_table.e2e.ts b/detox/e2e/test/messaging/markdown_table.e2e.ts index fd6b3f54f..0b760fef5 100644 --- a/detox/e2e/test/messaging/markdown_table.e2e.ts +++ b/detox/e2e/test/messaging/markdown_table.e2e.ts @@ -23,6 +23,7 @@ import { ServerScreen, TableScreen, } from '@support/ui/screen'; +import {isIos} from '@support/utils'; import {expect} from 'detox'; describe('Messaging - Markdown Table', () => { @@ -56,11 +57,11 @@ describe('Messaging - Markdown Table', () => { '|:---|:---|:---|\n' + '| 1 | Name | Toast |\n' + '| 2 | Name | Server |\n'; - await ChannelScreen.open(channelsCategory, testChannel.name); await Post.apiCreatePost(siteOneUrl, { channelId: testChannel.id, message: markdownTable, }); + await ChannelScreen.open(channelsCategory, testChannel.name); // * Verify markdown table is displayed 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' + '| :-- | :-: | --: |\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, { channelId: testChannel.id, message: markdownTable, }); + await ChannelScreen.open(channelsCategory, testChannel.name); // * Verify table is displayed with long text wrapped properly const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -119,11 +120,11 @@ describe('Messaging - Markdown Table', () => { '| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' + '| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(7) + '| Left | Center | Right | Right | Left | Center | Right | Right HS last |\n'; - await ChannelScreen.open(channelsCategory, testChannel.name); await Post.apiCreatePost(siteOneUrl, { channelId: testChannel.id, message: markdownTable, }); + await ChannelScreen.open(channelsCategory, testChannel.name); // * Verify table is displayed with some right columns not visible const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -156,11 +157,11 @@ describe('Messaging - Markdown Table', () => { '| :-- | :-: | --: |\n' + '| Left | Center | Right |\n'.repeat(30) + '| Left | Center | Right VS last |\n'; - await ChannelScreen.open(channelsCategory, testChannel.name); await Post.apiCreatePost(siteOneUrl, { channelId: testChannel.id, message: markdownTable, }); + await ChannelScreen.open(channelsCategory, testChannel.name); // * Verify table is displayed with some bottom rows not visible const {post} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); @@ -178,9 +179,13 @@ describe('Messaging - Markdown Table', () => { // * Verify table screen is scrollable to the bottom const expectedElement = element(by.text('Right VS last')); - await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(50, 'down'); - await expect(element(by.text('Header VS last'))).not.toBeVisible(); - await expect(expectedElement).toBeVisible(); + if (isIos()) { + await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'down'); + 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 await TableScreen.back(); @@ -194,11 +199,11 @@ describe('Messaging - Markdown Table', () => { '| :-- | :-: | --: | --: | :-- | :-: | --: | --: |\n' + '| Left | Center | Right | Right | Left | Center | Right | Right |\n'.repeat(30) + '| Left | Center | Right | Right | Left | Center | Right | Right last |\n'; - await ChannelScreen.open(channelsCategory, testChannel.name); await Post.apiCreatePost(siteOneUrl, { channelId: testChannel.id, message: markdownTable, }); + await ChannelScreen.open(channelsCategory, testChannel.name); // * Verify table is displayed with some right columns and bottom rows not visible 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(); // * Verify table screen is scrollable to the right and scrollable to the bottom - await TableScreen.tableScrollView.scrollTo('right'); - await expect(element(by.text('Header last'))).toBeVisible(); + await waitFor(element(by.text('Header last'))).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'right'); await expect(element(by.text('Right last'))).not.toBeVisible(); const expectedElement = element(by.text('Right last')); - await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(50, 'down'); - await expect(element(by.text('Header last'))).not.toBeVisible(); - await expect(expectedElement).toBeVisible(); + if (isIos()) { + await waitFor(expectedElement).toBeVisible().whileElement(by.id(TableScreen.testID.tableScrollView)).scroll(150, 'down'); + await expect(element(by.text('Header last'))).not.toBeVisible(); + await expect(expectedElement).toBeVisible(); + } else { + await expect(expectedElement).toExist(); + } // # Go back to channel list screen await TableScreen.back(); diff --git a/detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts b/detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts index 93d29a6bb..e2060c527 100644 --- a/detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts +++ b/detox/e2e/test/messaging/pin_and_unpin_message.e2e.ts @@ -68,7 +68,7 @@ describe('Messaging - Pin and Unpin Message', () => { await PostOptionsScreen.pinPostOption.tap(); // * 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); await expect(postListPostItemPreHeaderText).toHaveText(pinnedText); @@ -77,7 +77,7 @@ describe('Messaging - Pin and Unpin Message', () => { await PostOptionsScreen.unpinPostOption.tap(); // * 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(); // # Go back to channel list screen @@ -96,7 +96,7 @@ describe('Messaging - Pin and Unpin Message', () => { await PostOptionsScreen.pinPostOption.tap(); // * 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); await expect(postListPostItemPreHeaderText).toHaveText(pinnedText); @@ -105,7 +105,7 @@ describe('Messaging - Pin and Unpin Message', () => { await PostOptionsScreen.unpinPostOption.tap(); // * 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(); // # Go back to channel list screen diff --git a/detox/e2e/test/scheduled_messages/create_schedule_message.e2e.ts b/detox/e2e/test/scheduled_messages/create_schedule_message.e2e.ts new file mode 100644 index 000000000..770f2e259 --- /dev/null +++ b/detox/e2e/test/scheduled_messages/create_schedule_message.e2e.ts @@ -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(); + } +}); diff --git a/detox/e2e/test/search/search_messages.e2e.ts b/detox/e2e/test/search/search_messages.e2e.ts index 64bd9121c..ed783123f 100644 --- a/detox/e2e/test/search/search_messages.e2e.ts +++ b/detox/e2e/test/search/search_messages.e2e.ts @@ -96,7 +96,7 @@ describe('Search - Search Messages', () => { await SearchMessagesScreen.searchModifierFrom.tap(); await SearchMessagesScreen.searchInput.typeText(testUser.username); const {atMentionItem} = Autocomplete.getAtMentionItem(testUser.id); - await waitFor(atMentionItem).toBeVisible().withTimeout(2000); + await waitFor(atMentionItem).toBeVisible().withTimeout(timeouts.TWO_SEC); await atMentionItem.tap(); await SearchMessagesScreen.searchInput.tapReturnKey(); @@ -126,6 +126,7 @@ describe('Search - Search Messages', () => { await SearchMessagesScreen.searchModifierIn.tap(); await SearchMessagesScreen.searchInput.typeText(testChannel.name); const {channelMentionItem} = Autocomplete.getChannelMentionItem(testChannel.name); + await waitFor(channelMentionItem).toBeVisible().withTimeout(timeouts.TWO_SEC); await channelMentionItem.tap(); await SearchMessagesScreen.searchInput.tapReturnKey(); @@ -166,6 +167,7 @@ describe('Search - Search Messages', () => { await SearchMessagesScreen.searchInput.tapReturnKey(); // * Verify search results do not contain messages with excluded term + await wait(timeouts.TWO_SEC); await expect(nonExcludedPostListPostItem).toBeVisible(); await expect(excludedPostListPostItem).not.toBeVisible(); @@ -198,11 +200,15 @@ describe('Search - Search Messages', () => { await SearchMessagesScreen.searchModifierPhrases.tap(); await SearchMessagesScreen.searchInput.tapBackspaceKey(); await SearchMessagesScreen.searchInput.typeText(messageWithIncludedTerm); + + // # Collapse the keyboard + await element(by.id('search.modifier.header')).tap(); await SearchMessagesScreen.searchModifierPhrases.tap(); await SearchMessagesScreen.searchInput.tapBackspaceKey(); await SearchMessagesScreen.searchInput.tapReturnKey(); // * Verify search results only contain messages with included term + await wait(timeouts.TWO_SEC); await expect(nonIncludedPostListPostItem).not.toBeVisible(); await expect(includedPostListPostItem).toBeVisible(); @@ -223,17 +229,10 @@ describe('Search - Search Messages', () => { // * Verify on search messages screen 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 - await SearchMessagesScreen.searchModifierFrom.tap(); - 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(); + // # Tap on from-search-modifier, type in username + await SearchMessagesScreen.searchInput.typeText(`from: ${testUser.username} channel: ${testChannel.name}`); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); // * Verify search results only contain messages from user in channel 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 await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TEN_SEC); // * Verify search results contain searched message 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 await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); // * Verify search results contain searched 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 await SearchMessagesScreen.teamPickerButton.tap(); + await wait(timeouts.TWO_SEC); await TeamDropdownMenuScreen.getTeamIcon(testTeam.id).tap(); // * Verify search results do not contain searched message @@ -347,6 +349,7 @@ describe('Search - Search Messages', () => { const searchTerm = getRandomId(); await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); // * Verify empty search state for search messages 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 await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); + const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); 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 await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); 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 await SearchMessagesScreen.searchInput.typeText(searchTerm); await SearchMessagesScreen.searchInput.tapReturnKey(); + await wait(timeouts.TWO_SEC); + const {post: searchedPost} = await Post.apiGetLastPostInChannel(siteOneUrl, testChannel.id); await SearchMessagesScreen.openPostOptionsFor(searchedPost.id, message); await PostOptionsScreen.pinPostOption.tap(); diff --git a/detox/e2e/test/server_login/connect_to_server.e2e.ts b/detox/e2e/test/server_login/connect_to_server.e2e.ts index 57fb7a895..1fbdd720e 100644 --- a/detox/e2e/test/server_login/connect_to_server.e2e.ts +++ b/detox/e2e/test/server_login/connect_to_server.e2e.ts @@ -78,7 +78,11 @@ describe('Server Login - Connect to Server', () => { // * Verify invalid url error 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 () => { diff --git a/detox/e2e/test/server_login/login_by_email.e2e.ts b/detox/e2e/test/server_login/login_by_email.e2e.ts index e58704a59..6bc627642 100644 --- a/detox/e2e/test/server_login/login_by_email.e2e.ts +++ b/detox/e2e/test/server_login/login_by_email.e2e.ts @@ -17,6 +17,7 @@ import { LoginScreen, ServerScreen, } from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; import {expect} from 'detox'; 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 () => { // # Log in with empty username and non-empty password await usernameInput.clearText(); - await passwordInput.typeText('password'); + await passwordInput.replaceText('password'); + await wait(timeouts.TWO_SEC); // * Verify signin button is disabled await expect(signinButtonDisabled).toBeVisible(); // # Log in with non-empty username and empty password - await usernameInput.typeText('username'); + await usernameInput.replaceText('username'); await passwordInput.clearText(); + await LoginScreen.loginFormInfoText.tap(); // * Verify signin button is disabled 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 () => { // # Log in with incorrect credentials - await usernameInput.typeText('username'); - await passwordInput.typeText('password'); + await usernameInput.replaceText('username'); + await passwordInput.replaceText('password'); + await wait(timeouts.TWO_SEC); + await LoginScreen.loginFormInfoText.tap(); await signinButton.tap(); // * 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 () => { // # Log in to server with correct credentials const {team, user} = await Setup.apiInit(siteOneUrl); - await usernameInput.typeText(user.newUser.username); - await passwordInput.typeText(user.newUser.password); + await usernameInput.replaceText(user.newUser.username); + await passwordInput.replaceText(user.newUser.password); + await LoginScreen.loginFormInfoText.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 await ChannelListScreen.toBeVisible(); diff --git a/detox/e2e/test/server_login/server_list.e2e.ts b/detox/e2e/test/server_login/server_list.e2e.ts index 91239db86..796603eba 100644 --- a/detox/e2e/test/server_login/server_list.e2e.ts +++ b/detox/e2e/test/server_login/server_list.e2e.ts @@ -87,8 +87,9 @@ describe('Server Login - Server List', () => { // # Add a second server and log in to the second server await User.apiAdminLogin(siteTwoUrl); ({user: serverTwoUser} = await Setup.apiInit(siteTwoUrl)); + await wait(timeouts.TWO_SEC); await ServerListScreen.addServerButton.tap(); - await expect(ServerScreen.headerTitleAddServer).toBeVisible(); + await expect(ServerScreen.headerTitleAddServer).toBeVisible(35); await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName); await LoginScreen.login(serverTwoUser); @@ -112,8 +113,9 @@ describe('Server Login - Server List', () => { // # Add a third server and log in to the third server await User.apiAdminLogin(siteThreeUrl); ({user: serverThreeUser} = await Setup.apiInit(siteThreeUrl)); + await wait(timeouts.TWO_SEC); await ServerListScreen.addServerButton.tap(); - await expect(ServerScreen.headerTitleAddServer).toBeVisible(); + await expect(ServerScreen.headerTitleAddServer).toBeVisible(35); await ServerScreen.connectToServer(serverThreeUrl, serverThreeDisplayName); await LoginScreen.login(serverThreeUser); @@ -123,6 +125,7 @@ describe('Server Login - Server List', () => { // # Open server list screen await ServerListScreen.open(); + await wait(timeouts.TWO_SEC); if (isIos()) { await ServerListScreen.serverListTitle.swipe('up'); } 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 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(); // * Verify on edit server screen @@ -209,7 +212,7 @@ describe('Server Login - Server List', () => { await expect(ServerListScreen.getServerItemActive(newServerOneDisplayName)).toBeVisible(); // # 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 EditServerScreen.serverDisplayNameInput.replaceText(serverOneDisplayName); 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 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(); // * 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 - await Alert.removeButton3.tap(); + await waitFor(Alert.removeButton1).toBeVisible().withTimeout(timeouts.HALF_MIN); + await Alert.removeButton1.tap(); await wait(timeouts.ONE_SEC); await ServerListScreen.open(); if (isIos()) { await ServerListScreen.serverListTitle.swipe('up'); } else if (isAndroid()) { - await waitFor(ServerListScreen.serverListTitle).toBeVisible().withTimeout(timeouts.TWO_SEC); 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 await ServerListScreen.addServerButton.tap(); - await expect(ServerScreen.headerTitleAddServer).toBeVisible(); + await expect(ServerScreen.headerTitleAddServer).toBeVisible(35); await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); 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 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(); // * 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 - await Alert.logoutButton3.tap(); + await waitFor(Alert.logoutButton).toBeVisible().withTimeout(timeouts.TEN_SEC); + await Alert.logoutButton.tap(); // * 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(); // # 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.addServerButton.tap(); - await expect(ServerScreen.headerTitleAddServer).toBeVisible(); + await expect(ServerScreen.headerTitleAddServer).toBeVisible(35); await ServerScreen.serverUrlInput.replaceText(serverTwoUrl); if (isAndroid()) { 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 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 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(); }); }); diff --git a/detox/e2e/test/setup.ts b/detox/e2e/test/setup.ts index edd07e3b7..59c8d232e 100644 --- a/detox/e2e/test/setup.ts +++ b/detox/e2e/test/setup.ts @@ -1,23 +1,100 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +/* eslint-disable no-await-in-loop, no-console */ import {Plugin, System, User} from '@support/server_api'; 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} + */ +export async function launchAppWithRetry(): Promise { + 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 () => { // Login as sysadmin and reset server configuration await System.apiCheckSystemHealth(siteOneUrl); await User.apiAdminLogin(siteOneUrl); await Plugin.apiDisableNonPrepackagedPlugins(siteOneUrl); - - await device.launchApp({ - newInstance: true, - launchArgs: {detoxPrintBusyIdleResources: 'YES'}, - permissions: { - notifications: 'YES', - camera: 'YES', - medialibrary: 'YES', - photos: 'YES', - }, - }); + await launchAppWithRetry(); +}); + +// Add this to speed up test cleanup +afterAll(async () => { + try { + await device.terminateApp(); + } catch (error) { + console.error('Error terminating app:', error); + } }); diff --git a/detox/e2e/test/smoke_test/channels.e2e.ts b/detox/e2e/test/smoke_test/channels.e2e.ts index 21cf7e229..06fc8c48f 100644 --- a/detox/e2e/test/smoke_test/channels.e2e.ts +++ b/detox/e2e/test/smoke_test/channels.e2e.ts @@ -66,8 +66,10 @@ describe('Smoke Test - Channels', () => { const {channel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id}); await BrowseChannelsScreen.open(); 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 ChannelScreen.closeScheduledMessageTooltip(); // * Verify on newly joined channel screen await ChannelScreen.toBeVisible(); @@ -90,8 +92,9 @@ describe('Smoke Test - Channels', () => { // # Open create channel screen and create a new channel const displayName = `Channel ${getRandomId()}`; await CreateOrEditChannelScreen.openCreateChannel(); - await CreateOrEditChannelScreen.displayNameInput.replaceText(displayName); - await CreateOrEditChannelScreen.createButton.tap(); + await CreateOrEditChannelScreen.displayNameInput.typeText(displayName); + await wait(timeouts.FOUR_SEC); + await CreateOrEditChannelScreen.clickonCreateButton(); // * Verify on newly created public channel await ChannelScreen.toBeVisible(); @@ -106,8 +109,10 @@ describe('Smoke Test - Channels', () => { await CreateDirectMessageScreen.open(); await CreateDirectMessageScreen.closeTutorial(); await CreateDirectMessageScreen.searchInput.replaceText(newUserDisplayName); + await CreateDirectMessageScreen.searchInput.tapReturnKey(); await wait(timeouts.ONE_SEC); await CreateDirectMessageScreen.getUserItem(newUser.id).tap(); + await wait(timeouts.ONE_SEC); await CreateDirectMessageScreen.startButton.tap(); // * Verify on direct message channel screen for the new user @@ -134,11 +139,13 @@ describe('Smoke Test - Channels', () => { 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 await FindChannelsScreen.open(); 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(); // * Verify on target channel screen diff --git a/detox/e2e/test/smoke_test/server_login.e2e.ts b/detox/e2e/test/smoke_test/server_login.e2e.ts index 18ce602f5..b43ae4d92 100644 --- a/detox/e2e/test/smoke_test/server_login.e2e.ts +++ b/detox/e2e/test/smoke_test/server_login.e2e.ts @@ -25,7 +25,7 @@ import { ServerListScreen, ServerScreen, } from '@support/ui/screen'; -import {isAndroid, isIos, timeouts} from '@support/utils'; +import {isAndroid, isIos, timeouts, wait} from '@support/utils'; import {expect} from 'detox'; describe('Smoke Test - Server Login', () => { @@ -69,7 +69,7 @@ describe('Smoke Test - Server Login', () => { await User.apiAdminLogin(siteTwoUrl); const {user} = await Setup.apiInit(siteTwoUrl); await ServerListScreen.addServerButton.tap(); - await expect(ServerScreen.headerTitleAddServer).toBeVisible(); + await expect(ServerScreen.headerTitleAddServer).toBeVisible(35); await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName); await LoginScreen.login(user); @@ -102,6 +102,7 @@ describe('Smoke Test - Server Login', () => { // # Tap on logout button await Alert.logoutButton.tap(); + await wait(timeouts.FOUR_SEC); // * Verify second server is logged out await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left'); diff --git a/detox/e2e/test/teams/invite_people.e2e.ts b/detox/e2e/test/teams/invite_people.e2e.ts index 5ea20aa33..91b67f5ad 100644 --- a/detox/e2e/test/teams/invite_people.e2e.ts +++ b/detox/e2e/test/teams/invite_people.e2e.ts @@ -73,9 +73,6 @@ describe('Teams - Invite', () => { await expect(Invite.teamDisplayName).toHaveText(testTeam.display_name); await expect(Invite.teamIcon).toBeVisible(); - // * Verify default Selection - await waitFor(Invite.screenSelection).toBeVisible().withTimeout(timeouts.FOUR_SEC); - // * Verify Server data await expect(Invite.serverDisplayName).toHaveText(serverOneDisplayName); @@ -139,7 +136,8 @@ describe('Teams - Invite', () => { await expect(Invite.getSearchListUserItem(testUser1.id)).not.toBeVisible(); // * 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 await Invite.sendButton.tap(); @@ -163,7 +161,8 @@ describe('Teams - Invite', () => { await Invite.getSearchListUserItem(testUser2.id).tap(); // * 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 await Invite.sendButton.tap(); @@ -190,7 +189,8 @@ describe('Teams - Invite', () => { await Invite.getSearchListUserItem(testUser3.id).tap(); // * 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 await Invite.searchBarInput.replaceText(testUser.username); @@ -201,9 +201,6 @@ describe('Teams - Invite', () => { // # Select user item await Invite.getSearchListUserItem(testUser.id).tap(); - // * Validate user is added to selected items - await expect(Invite.getSelectedItem(testUser.id)).toBeVisible(); - // # Send invitation await Invite.sendButton.tap(); diff --git a/detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts b/detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts index 6d880c101..b562da73e 100644 --- a/detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts +++ b/detox/e2e/test/threads/follow_and_unfollow_thread.e2e.ts @@ -186,6 +186,8 @@ describe('Threads - Follow and Unfollow Thread', () => { await wait(timeouts.ONE_SEC); await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).not.toBeVisible(); + await wait(timeouts.FOUR_SEC); + // # Go back to channel list screen await GlobalThreadsScreen.back(); }); diff --git a/detox/e2e/test/threads/global_threads.e2e.ts b/detox/e2e/test/threads/global_threads.e2e.ts index 0fb42b0e0..1e3637555 100644 --- a/detox/e2e/test/threads/global_threads.e2e.ts +++ b/detox/e2e/test/threads/global_threads.e2e.ts @@ -24,7 +24,7 @@ import { ServerScreen, ThreadScreen, } from '@support/ui/screen'; -import {getRandomId} from '@support/utils'; +import {getRandomId, timeouts} from '@support/utils'; import {expect} from 'detox'; describe('Threads - Global Threads', () => { @@ -67,7 +67,7 @@ describe('Threads - Global Threads', () => { 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 const parentMessage = `Message ${getRandomId()}`; 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 await ThreadScreen.back(); await ChannelScreen.back(); - await device.reloadReactNative(); await GlobalThreadsScreen.open(); await GlobalThreadsScreen.headerAllThreadsButton.tap(); @@ -93,7 +92,13 @@ describe('Threads - Global Threads', () => { await expect(GlobalThreadsScreen.getThreadItem(parentPost.id)).toBeVisible(); await expect(GlobalThreadsScreen.getThreadItemThreadStarterUserDisplayName(parentPost.id)).toHaveText(testUser.username); 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 await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); @@ -133,7 +138,7 @@ describe('Threads - Global Threads', () => { 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 const parentMessage = `Message ${getRandomId()}`; 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 await ThreadScreen.back(); await ChannelScreen.back(); - await device.reloadReactNative(); await GlobalThreadsScreen.open(); await GlobalThreadsScreen.headerAllThreadsButton.tap(); // * Verify the thread replied to by the current user is displayed 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.getThreadItemFooterReplyCount(parentPost.id)).toHaveText('1 reply'); // # Tap on the thread await GlobalThreadsScreen.getThreadItem(parentPost.id).tap(); diff --git a/detox/e2e/test/threads/save_and_unsave_thread.e2e.ts b/detox/e2e/test/threads/save_and_unsave_thread.e2e.ts index 4ef5ce187..5841784a6 100644 --- a/detox/e2e/test/threads/save_and_unsave_thread.e2e.ts +++ b/detox/e2e/test/threads/save_and_unsave_thread.e2e.ts @@ -119,9 +119,6 @@ describe('Threads - Save and Unsave Thread', () => { // # Tap on thread overview unsave button 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 await ThreadScreen.back(); await ChannelScreen.back(); diff --git a/detox/inject-detox-settings.js b/detox/inject-detox-settings.js new file mode 100644 index 000000000..135d91637 --- /dev/null +++ b/detox/inject-detox-settings.js @@ -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 = ` + + + + + + +`; + +// 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 = ` + + + + + + +`; + +// 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(); diff --git a/detox/package-lock.json b/detox/package-lock.json index ddc844d8a..17c3cd665 100644 --- a/detox/package-lock.json +++ b/detox/package-lock.json @@ -23,7 +23,7 @@ "babel-plugin-module-resolver": "5.0.2", "client-oauth2": "4.3.3", "deepmerge": "4.3.1", - "detox": "20.33.0", + "detox": "20.37.0", "form-data": "4.0.1", "jest": "29.7.0", "jest-circus": "29.7.0", @@ -1098,14 +1098,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1145,18 +1144,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz", + "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^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/traverse": "^7.25.9", + "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "engines": { @@ -1261,11 +1259,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -1289,15 +1286,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1380,13 +1376,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -2753,32 +2748,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2787,11 +2780,10 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -2807,6 +2799,26 @@ "dev": true, "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": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", @@ -4276,6 +4288,12 @@ "dev": true, "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": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -4300,6 +4318,37 @@ "dev": true, "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4932,18 +4981,6 @@ "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": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5014,6 +5051,16 @@ "dev": true, "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5034,6 +5081,41 @@ "dev": true, "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5232,19 +5314,20 @@ } }, "node_modules/detox": { - "version": "20.33.0", - "resolved": "https://registry.npmjs.org/detox/-/detox-20.33.0.tgz", - "integrity": "sha512-9KZC3NgCav/IvuWm/EaKNIL5lV2bJf23PPK67OHS2dMhJsDsNBRWP9k3RN7hzLzzUwAXMMoOmk+S1OpOsazNFA==", + "version": "20.37.0", + "resolved": "https://registry.npmjs.org/detox/-/detox-20.37.0.tgz", + "integrity": "sha512-OUI2p3z31Yku1USBia0jrlwoQoa6rANvVWuUSJyjBlRRKDSeOf/DG/ivk+R5FO+lYZZtA3pIbl6LSql/Bf6DpQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { + "@wix-pilot/core": "^3.2.2", + "@wix-pilot/detox": "^1.0.11", "ajv": "^8.6.3", "bunyan": "^1.8.12", "bunyan-debug-stream": "^3.1.0", "caf": "^15.0.1", "chalk": "^4.0.0", - "child-process-promise": "^2.2.0", - "detox-copilot": "^0.0.27", "execa": "^5.1.1", "find-up": "^5.0.0", "fs-extra": "^11.0.0", @@ -5257,6 +5340,7 @@ "multi-sort-stream": "^1.0.3", "multipipe": "^4.0.0", "node-ipc": "9.2.1", + "promisify-child-process": "^4.1.2", "proper-lockfile": "^3.0.2", "resolve-from": "^5.0.0", "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5508,6 +5585,12 @@ "dev": true, "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5723,6 +5806,12 @@ "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -5793,6 +5882,12 @@ "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": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -6159,8 +6254,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/interpret": { "version": "1.4.0", @@ -7408,6 +7502,12 @@ "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7466,6 +7566,23 @@ "dev": true, "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": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7789,16 +7906,6 @@ "dev": true, "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": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", @@ -7858,6 +7965,15 @@ "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": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -8159,6 +8275,15 @@ "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": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", @@ -8322,12 +8447,14 @@ "dev": true, "license": "MIT" }, - "node_modules/promise-polyfill": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", - "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", + "node_modules/promisify-child-process": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/promisify-child-process/-/promisify-child-process-4.1.2.tgz", + "integrity": "sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=8" + } }, "node_modules/prompts": { "version": "2.4.2", @@ -8816,6 +8943,15 @@ "license": "MIT", "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": { "version": "1.6.3", "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", "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8991,6 +9126,21 @@ "dev": true, "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -9036,6 +9186,15 @@ "dev": true, "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": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9327,6 +9486,12 @@ "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": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz", @@ -9400,6 +9565,15 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -9692,6 +9866,70 @@ "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": { "version": "7.0.0", "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", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.3.0" }, diff --git a/detox/package.json b/detox/package.json index 56cec5982..52143c65d 100644 --- a/detox/package.json +++ b/detox/package.json @@ -21,7 +21,7 @@ "babel-plugin-module-resolver": "5.0.2", "client-oauth2": "4.3.3", "deepmerge": "4.3.1", - "detox": "20.33.0", + "detox": "20.37.0", "form-data": "4.0.1", "jest": "29.7.0", "jest-circus": "29.7.0", @@ -42,13 +42,14 @@ "xml2js": "0.6.2" }, "scripts": { + "e2e:android-inject-settings": "node inject-detox-settings.js", "e2e:android-create-emulator": "./create_android_emulator.sh", "e2e:android-build": "detox build -c android.emu.debug", - "e2e:android-test": "detox test -c android.emu.debug", + "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-test-release": "detox test -c android.emu.release --record-logs failing --take-screenshots failing", "e2e:ios-build": "IOS=true detox build -c ios.sim.debug", - "e2e:ios-test": "IOS=true detox test -c ios.sim.debug", + "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-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", diff --git a/detox/save_report.js b/detox/save_report.js index 99348a913..a398151c1 100644 --- a/detox/save_report.js +++ b/detox/save_report.js @@ -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 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) { // 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) { console.log('Successfully uploaded artifacts to S3:', result.reportLink); } diff --git a/detox/utils/artifacts.js b/detox/utils/artifacts.js index 0778defb0..a093cf166 100644 --- a/detox/utils/artifacts.js +++ b/detox/utils/artifacts.js @@ -20,10 +20,8 @@ const { DETOX_AWS_S3_BUCKET, DETOX_AWS_ACCESS_KEY_ID, DETOX_AWS_SECRET_ACCESS_KEY, - IOS, REPORT_PATH, } = process.env; -const platform = IOS === 'true' ? 'ios' : 'android'; const s3 = new S3({ credentials: { @@ -36,7 +34,7 @@ function getFiles(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) { console.log('No AWS credentials found. Test artifacts not uploaded to S3.'); diff --git a/detox/utils/generate_detox_config_ci.js b/detox/utils/generate_detox_config_ci.js index 910e7bbab..da761a5bd 100644 --- a/detox/utils/generate_detox_config_ci.js +++ b/detox/utils/generate_detox_config_ci.js @@ -4,8 +4,8 @@ /* eslint-disable no-process-env */ /* eslint-disable no-console */ const fs = require('fs'); -const deviceName = process.env.DEVICE_NAME || 'iPhone 16 Pro'; -const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 18.1'; +const deviceName = process.env.DEVICE_NAME || 'iPhone 15 Pro'; +const deviceOSVersion = process.env.DEVICE_OS_VERSION || 'iOS 17.2'; const detoxConfigTemplate = fs.readFileSync('../.detoxrc.json', 'utf8'); const detoxConfig = detoxConfigTemplate.replace('__DEVICE_NAME__', deviceName).replace('__DEVICE_OS_VERSION__', deviceOSVersion); diff --git a/detox/utils/jest_stare.js b/detox/utils/jest_stare.js index bb03b0f75..115912ac2 100644 --- a/detox/utils/jest_stare.js +++ b/detox/utils/jest_stare.js @@ -129,9 +129,9 @@ function collectReportSuites(reports) { }; } -function generateJestStareHtmlReport(outputDir, outputFile, inputFilePath) { +function generateJestStareHtmlReport(outputDir, outputFile, inputFilePath, platform) { 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) { diff --git a/env.d.ts b/env.d.ts index 97393325c..692ccdb6e 100644 --- a/env.d.ts +++ b/env.d.ts @@ -3,6 +3,7 @@ // So that typescript doesn't complain about importing `@env` through react-native-dotenv declare module '@env' { + export const RUNNING_E2E: string; } declare module 'react-native-dotenv' { diff --git a/index.ts b/index.ts index 2a87c4cd2..c94377789 100644 --- a/index.ts +++ b/index.ts @@ -1,10 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {RUNNING_E2E} from '@env'; import TurboLogger from '@mattermost/react-native-turbo-log'; import {LogBox, Platform, UIManager} from 'react-native'; import ViewReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes'; -import {RUNNING_E2E} from 'react-native-dotenv'; import 'react-native-gesture-handler'; import {Navigation} from 'react-native-navigation';