Add ability to run detox tests on Android (#8553)

This commit is contained in:
yasser khan 2025-02-14 01:26:53 +05:30 committed by GitHub
parent 985fa692a6
commit 9976cacec4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 907 additions and 394 deletions

View file

@ -52,20 +52,22 @@ class Specs {
}
generateSplits() {
const chunkSize = Math.ceil(this.rawFiles.length / this.parallelism);
const chunkSize = Math.floor(this.rawFiles.length / this.parallelism);
let remainder = this.rawFiles.length % this.parallelism;
let runNo = 1;
let start = 0;
for (let i = 0; i < this.rawFiles.length; i += chunkSize) {
const end = Math.min(i + chunkSize, this.rawFiles.length);
const fileGroup = this.rawFiles.slice(i, end).join(' ');
for (let i = 0; i < this.parallelism; i++) {
let end = start + chunkSize + (remainder > 0 ? 1 : 0);
const fileGroup = this.rawFiles.slice(start, end).join(' ');
const specFileGroup = new SpecGroup(runNo.toString(), fileGroup, this.deviceInfo);
this.groupedFiles.push(specFileGroup);
if (end === this.rawFiles.length) {
break;
}
start = end;
runNo++;
if (remainder > 0) {
remainder--;
}
}
}

View file

@ -1,73 +0,0 @@
name: Detox E2E Android Tests PR
on:
pull_request:
branches:
- main
types:
- labeled
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event.label.name }}"
cancel-in-progress: true
jobs:
build-android-apk:
if: github.event.label.name == 'E2E Android tests for PR'
runs-on: ubuntu-latest-8-cores
env:
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
steps:
- name: Prune Docker to free up space
run: docker system prune -af
- name: Remove npm Temporary Files
run: |
rm -rf ~/.npm/_cacache
- name: ci/checkout-repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Dependencies
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: ~/.gradle/caches/modules-2/
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v3
- name: Inject Detox settings
run: cd detox && npm run e2e:android-inject-settings
- name: Update minSdkVersion for react-native-image-picker
run: |
sed -i 's/minSdkVersion 21/minSdkVersion 23/' ./node_modules/react-native-image-picker/android/build.gradle
cat ./node_modules/react-native-image-picker/android/build.gradle | grep minSdkVersion
- name: Detox build
run: |
cd detox
npm install
npm install -g detox-cli
npm run e2e:android-build
- name: ci/upload-android-pr-build
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-apk-${{ github.run_id }}
path: "android/app/build/outputs/apk/**/app-*.apk"

View file

@ -0,0 +1,295 @@
name: Detox Android E2E Tests Template
on:
workflow_call:
inputs:
MM_TEST_SERVER_URL:
description: "The test server URL"
required: false
type: string
MM_TEST_USER_NAME:
description: "The admin username of the test instance"
required: false
type: string
MM_TEST_PASSWORD:
description: "The admin password of the test instance"
required: false
type: string
MOBILE_VERSION:
description: "The mobile version to test"
required: false
default: ${{ github.head_ref || github.ref }}
type: string
run-android-tests:
description: "Run Android tests"
required: true
type: boolean
run-type:
type: string
required: false
default: "PR"
testcase_failure_fatal:
description: "Should failures be considered fatal"
required: false
type: boolean
default: false
record_tests_in_zephyr:
description: "Record test results in Zephyr, typically for nightly and release runs"
required: false
type: string
default: 'false'
low_bandwidth_mode:
description: "Enable low bandwidth mode"
required: false
type: boolean
default: false
android_avd_name:
description: "Android Emulator name"
required: false
type: string
default: "detox_pixel_4_xl"
android_api_level:
description: "Android API level"
required: false
type: string
default: "33"
outputs:
STATUS:
value: ${{ jobs.generate-report.outputs.STATUS }}
TARGET_URL:
value: ${{ jobs.generate-report.outputs.TARGET_URL }}
FAILURES:
value: ${{ jobs.generate-report.outputs.FAILURES }}
env:
AWS_REGION: "us-east-1"
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
COMMIT_HASH: ${{ github.sha }}
DEVICE_NAME: ${{ inputs.android_avd_name }} # This is needed to split tests as same code is used in iOS job
DEVICE_OS_VERSION: ${{ inputs.android_api_level }}
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
HEADLESS: "true"
TYPE: ${{ inputs.run-type }}
PULL_REQUEST: "https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}"
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL || 'https://mobile-e2e-site-1.test.mattermost.cloud' }}
SITE_2_URL: "https://mobile-e2e-site-2.test.mattermost.cloud"
SITE_3_URL: "https://mobile-e2e-site-3.test.mattermost.cloud"
ZEPHYR_ENABLE: ${{ inputs.record_tests_in_zephyr }}
JIRA_PROJECT_KEY: "MM"
ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }}
ZEPHYR_FOLDER_ID: "3233873"
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
FAILURE_MESSAGE: "Something has failed"
IOS: "false"
RUNNING_E2E: "true"
AVD_NAME: ${{ inputs.android_avd_name }}
SDK_VERSION: ${{ inputs.android_api_level }}
jobs:
generate-specs:
runs-on: ubuntu-22.04
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
mobile_sha: ${{ steps.resolve-device.outputs.MOBILE_SHA }}
mobile_ref: ${{ steps.resolve-device.outputs.MOBILE_REF }}
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Set Build ID
id: resolve-device
run: |
BUILD_ID="${{ github.run_id }}-${{ env.AVD_NAME }}-${{ env.SDK_VERSION}}"
WORKFLOW_HASH=$(tr -dc a-z0-9 </dev/urandom | head -c 10)
## We need that hash to separate the artifacts
echo "WORKFLOW_HASH=${WORKFLOW_HASH}" >> ${GITHUB_OUTPUT}
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> ${GITHUB_OUTPUT}
echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
echo "MOBILE_REF=$(git rev-parse --abbrev-ref HEAD)" >> ${GITHUB_OUTPUT}
- name: Generate Test Specs
id: generate-specs
uses: ./.github/actions/generate-specs
with:
parallelism: 10
search_path: detox/e2e/test
device_name: ${{ env.AVD_NAME }}
device_os_version: ${{ env.SDK_VERSION }}
e2e-android:
name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
runs-on: ubuntu-latest-8-cores
continue-on-error: true
timeout-minutes: 150
env:
ANDROID_HOME: /usr/local/lib/android/sdk
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
needs:
- generate-specs
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpulse0
sudo apt-get install -y scrot ffmpeg xvfb
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Prepare Android Build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Detox Dependencies
run: |
cd detox
npm install
- name: Create destination path
run: mkdir -p android/app/build
- name: Download APK artifact
uses: actions/download-artifact@v4
with:
name: android-build-files-${{ github.run_id }}
path: android/app/build
- name: Set up Android SDK
run: |
export ANDROID_HOME=/usr/local/lib/android/sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
echo "PATH=$PATH" >> $GITHUB_ENV
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Accept Android licenses
run: |
yes | sdkmanager --licenses || true
- name: Install Android system image
run: |
sdkmanager "system-images;android-34;default;x86_64"
sdkmanager "platform-tools" "emulator"
- 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 }}
continue-on-error: true # We want to run all the tests
- name: Upload Android Test Report
if: always()
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
path: detox/artifacts/
generate-report:
runs-on: ubuntu-22.04
needs:
- generate-specs
- e2e-android
outputs:
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
STATUS: ${{ steps.determine-status.outputs.STATUS }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Download Android Artifacts
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
path: detox/artifacts/
pattern: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
continue-on-error: true
- name: Generate Report Path
id: s3
run: |
path="${{ needs.generate-specs.outputs.build_id }}-${{ needs.generate-specs.outputs.mobile_sha }}-${{ needs.generate-specs.outputs.mobile_ref }}"
echo "path=$(echo "${path}" | sed 's/\./-/g')" >> ${GITHUB_OUTPUT}
- name: Save report Detox Dependencies
id: report-link
run: |
cd detox
npm ci
npm run e2e:save-report
env:
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
REPORT_PATH: ${{ steps.s3.outputs.path }}
## These are needed for the MM Webhook report
COMMIT_HASH: ${{ needs.generate-specs.outputs.mobile_sha }}
BRANCH: ${{ needs.generate-specs.outputs.mobile_ref }}
- name: Calculate failures
id: summary
run: |
echo "FAILURES=$(cat detox/artifacts/summary.json | jq .stats.failures)" >> ${GITHUB_OUTPUT}
echo "PASSES=$(cat detox/artifacts/summary.json | jq .stats.passes)" >> ${GITHUB_OUTPUT}
echo "SKIPPED=$(cat detox/artifacts/summary.json | jq .stats.skipped)" >> ${GITHUB_OUTPUT}
echo "TOTAL=$(cat detox/artifacts/summary.json | jq .stats.tests)" >> ${GITHUB_OUTPUT}
echo "ERRORS=$(cat detox/artifacts/summary.json | jq .stats.errors)" >> ${GITHUB_OUTPUT}
echo "PERCENTAGE=$(cat detox/artifacts/summary.json | jq .stats.passPercent)" >> ${GITHUB_OUTPUT}
- name: Set Target URL
id: set-url
run: |
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${{ steps.s3.outputs.path }}/jest-stare/android-report.html" >> ${GITHUB_OUTPUT}
- name: Determine Status
id: determine-status
run: |
if [[ ${{ steps.summary.outputs.failures }} -gt 0 && "${{ inputs.testcase_failure_fatal }}" == "true" ]]; then
echo "STATUS=failure" >> ${GITHUB_OUTPUT}
else
echo "STATUS=success" >> ${GITHUB_OUTPUT}
fi
- name: Generate Summary
run: |
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: | Errors :warning: | " >> ${GITHUB_STEP_SUMMARY}
echo "|:---:|:---:|:---:|:---:|:---:|" >> ${GITHUB_STEP_SUMMARY}
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} | ${{ steps.summary.outputs.ERRORS }} |" >> ${GITHUB_STEP_SUMMARY}
echo "" >> ${GITHUB_STEP_SUMMARY}
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}

View file

@ -14,9 +14,24 @@ concurrency:
cancel-in-progress: true
jobs:
update-initial-status:
runs-on: ubuntu-22.04
update-initial-status-ios:
if: contains(github.event.label.name, 'E2E iOS tests for PR')
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
if: contains(github.event.label.name, 'E2E iOS tests for PR')
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.event.pull_request.head.sha }}
context: e2e/detox-ios-tests
description: Detox iOS tests for mattermost mobile app have started ...
status: pending
update-initial-status-android:
runs-on: ubuntu-22.04
if: contains(github.event.label.name, 'E2E Android tests for PR')
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
@ -24,14 +39,15 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.event.pull_request.head.sha }}
context: e2e/detox-tests
description: Detox tests for mattermost mobile app have started ...
context: e2e/detox-android-tests
description: Detox Android tests for mattermost mobile app have started ...
status: pending
build-ios-simulator:
if: contains(github.event.label.name, 'E2E iOS tests for PR')
runs-on: macos-14
needs:
- update-initial-status
- update-initial-status-ios
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@ -54,20 +70,75 @@ jobs:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
build-android-apk:
runs-on: ubuntu-latest-8-cores
if: contains(github.event.label.name, 'E2E Android tests for PR')
needs:
- update-initial-status-android
env:
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
steps:
- name: Prune Docker to free up space
run: docker system prune -af
- name: Remove npm Temporary Files
run: |
rm -rf ~/.npm/_cacache
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Prepare Android Build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Dependencies
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
- name: Detox build
run: |
cd detox
npm install
npm install -g detox-cli
npm run e2e:android-build
- name: Upload Android Build
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-files-${{ github.run_id }}
path: "android/app/build/**/*"
run-ios-tests-on-pr:
if: contains(github.event.label.name, 'E2E iOS tests for PR')
name: iOS Mobile Tests on PR
uses: ./.github/workflows/e2e-detox-template.yml
uses: ./.github/workflows/e2e-ios-template.yml
needs:
- build-ios-simulator
with:
run-ios-tests: true
run-type: "PR"
MOBILE_VERSION: ${{ github.event.pull_request.head.sha }}
low_bandwidth_mode: ${{ contains(github.event.label.name,'LBW') && true || false }}
secrets: inherit
update-final-status:
run-android-tests-on-pr:
if: contains(github.event.label.name, 'E2E Android tests for PR')
name: Android Mobile Tests on PR
uses: ./.github/workflows/e2e-android-template.yml
needs:
- build-android-apk
with:
run-android-tests: true
run-type: "PR"
MOBILE_VERSION: ${{ github.event.pull_request.head.sha }}
secrets: inherit
update-final-status-ios:
runs-on: ubuntu-22.04
if: contains(github.event.label.name, 'E2E iOS tests for PR')
needs:
- run-ios-tests-on-pr
steps:
@ -77,12 +148,29 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.event.pull_request.head.sha }}
context: e2e/detox-tests
context: e2e/detox-ios-tests
description: Completed with ${{ needs.run-ios-tests-on-pr.outputs.FAILURES }} failures
status: ${{ needs.run-ios-tests-on-pr.outputs.STATUS }}
target_url: ${{ needs.run-ios-tests-on-pr.outputs.TARGET_URL }}
e2e-remove-label:
update-final-status-android:
runs-on: ubuntu-22.04
if: contains(github.event.label.name, 'E2E Android tests for PR')
needs:
- run-android-tests-on-pr
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.event.pull_request.head.sha }}
context: e2e/detox-android-tests
description: Completed with ${{ needs.run-android-tests-on-pr.outputs.FAILURES }} failures
status: ${{ needs.run-android-tests-on-pr.outputs.STATUS }}
target_url: ${{ needs.run-android-tests-on-pr.outputs.TARGET_URL }}
e2e-remove-ios-label:
runs-on: ubuntu-22.04
needs:
- run-ios-tests-on-pr
@ -93,7 +181,6 @@ jobs:
with:
script: |
const iosLabel = 'E2E iOS tests for PR';
const androidLabel = 'E2E Android tests for PR';
context.payload.pull_request.labels.forEach(label => {
if (label.name.includes(iosLabel)) {
github.rest.issues.removeLabel({
@ -104,3 +191,25 @@ jobs:
});
}
});
e2e-remove-android-label:
runs-on: ubuntu-22.04
needs:
- run-android-tests-on-pr
steps:
- name: e2e/remove-label-from-pr
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
continue-on-error: true # Label might have been removed manually
with:
script: |
const androidLabel = 'E2E Android tests for PR';
context.payload.pull_request.labels.forEach(label => {
if (label.name.includes(androidLabel)) {
github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
});
}
});

View file

@ -6,7 +6,7 @@ on:
- release-*
jobs:
update-initial-status:
update-initial-status-ios:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
@ -15,14 +15,27 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-tests
description: Detox tests for mattermost mobile app have started ...
context: e2e/detox-ios-tests
description: Detox iOS tests for mattermost mobile app have started ...
status: pending
update-initial-status-android:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-android-tests
description: Detox Android tests for mattermost mobile app have started ...
status: pending
build-ios-simulator:
runs-on: macos-14
needs:
- update-initial-status
- update-initial-status-ios
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@ -45,19 +58,80 @@ jobs:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
build-android-apk:
runs-on: ubuntu-latest-8-cores
needs:
- update-initial-status-android
env:
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
steps:
- name: Prune Docker to free up space
run: docker system prune -af
- name: Remove npm Temporary Files
run: |
rm -rf ~/.npm/_cacache
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Prepare Android Build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Dependencies
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: ~/.gradle/caches/modules-2/
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Detox build
run: |
cd detox
npm install
npm install -g detox-cli
npm run e2e:android-build
- name: Upload Android Build
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-files-${{ github.run_id }}
path: "android/app/build/**/*"
run-ios-tests-on-release:
name: iOS Mobile Tests on Release
uses: ./.github/workflows/e2e-detox-template.yml
uses: ./.github/workflows/e2e-ios-template.yml
needs:
- build-ios-simulator
with:
run-ios-tests: true
run-type: "RELEASE"
record_tests_in_zephyr: 'true'
MOBILE_VERSION: ${{ github.ref }}
secrets: inherit
update-final-status:
run-android-tests-on-release:
name: Android Mobile Tests on Release
uses: ./.github/workflows/e2e-android-template.yml
needs:
- build-android-apk
with:
run-android-tests: true
run-type: "RELEASE"
record_tests_in_zephyr: 'true'
MOBILE_VERSION: ${{ github.ref }}
secrets: inherit
update-final-status-ios:
runs-on: ubuntu-22.04
needs:
- run-ios-tests-on-release
@ -68,7 +142,23 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-tests
context: e2e/detox-ios-tests
description: Completed with ${{ needs.run-ios-tests-on-release.outputs.FAILURES }} failures
status: ${{ needs.run-ios-tests-on-release.outputs.STATUS }}
target_url: ${{ needs.run-ios-tests-on-release.outputs.TARGET_URL }}
update-final-status-android:
runs-on: ubuntu-22.04
needs:
- run-android-tests-on-release
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-android-tests
description: Completed with ${{ needs.run-android-tests-on-release.outputs.FAILURES }} failures
status: ${{ needs.run-android-tests-on-release.outputs.STATUS }}
target_url: ${{ needs.run-android-tests-on-release.outputs.TARGET_URL }}

View file

@ -5,7 +5,7 @@ on:
- cron: "0 0 * * 4,5" # Wednesday and Thursday midnight
jobs:
update-initial-status:
update-initial-status-ios:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
@ -14,14 +14,27 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-tests
description: Detox tests for mattermost mobile app have started ...
context: e2e/detox-ios-tests
description: Detox iOS tests for mattermost mobile app have started ...
status: pending
update-initial-status-android:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-android-tests
description: Detox Android tests for mattermost mobile app have started ...
status: pending
build-ios-simulator:
runs-on: macos-14
needs:
- update-initial-status
- update-initial-status-ios
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@ -44,19 +57,80 @@ jobs:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
build-android-apk:
runs-on: ubuntu-latest-8-cores
needs:
- update-initial-status-android
env:
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
steps:
- name: Prune Docker to free up space
run: docker system prune -af
- name: Remove npm Temporary Files
run: |
rm -rf ~/.npm/_cacache
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Prepare Android Build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Dependencies
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
- name: Cache Gradle dependencies
uses: actions/cache@v4
with:
path: ~/.gradle/caches/modules-2/
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Detox build
run: |
cd detox
npm install
npm install -g detox-cli
npm run e2e:android-build
- name: Upload Android Build
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-files-${{ github.run_id }}
path: "android/app/build/**/*"
run-ios-tests-on-main-scheduled:
name: iOS Mobile Tests on Main (Scheduled)
uses: ./.github/workflows/e2e-detox-template.yml
uses: ./.github/workflows/e2e-ios-template.yml
needs:
- build-ios-simulator
with:
run-ios-tests: true
run-type: "MAIN"
record_tests_in_zephyr: 'true'
MOBILE_VERSION: ${{ github.ref }}
secrets: inherit
update-final-status:
run-android-tests-on-main-scheduled:
name: Android Mobile Tests on Main (Scheduled)
uses: ./.github/workflows/e2e-android-template.yml
needs:
- build-android-apk
with:
run-android-tests: true
run-type: "MAIN"
record_tests_in_zephyr: 'true'
MOBILE_VERSION: ${{ github.ref }}
secrets: inherit
update-final-status-ios:
runs-on: ubuntu-22.04
needs:
- run-ios-tests-on-main-scheduled
@ -67,7 +141,23 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-tests
context: e2e/detox-ios-tests
description: Completed with ${{ needs.run-ios-tests-on-main-scheduled.outputs.FAILURES }} failures
status: ${{ needs.run-ios-tests-on-main-scheduled.outputs.STATUS }}
target_url: ${{ needs.run-ios-tests-on-main-scheduled.outputs.TARGET_URL }}
update-final-status-android:
runs-on: ubuntu-22.04
needs:
- run-android-tests-on-main-scheduled
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-android-tests
description: Completed with ${{ needs.run-android-tests-on-main-scheduled.outputs.FAILURES }} failures
status: ${{ needs.run-android-tests-on-main-scheduled.outputs.STATUS }}
target_url: ${{ needs.run-android-tests-on-main-scheduled.outputs.TARGET_URL }}

View file

@ -1,4 +1,4 @@
name: Detox E2E Tests Template
name: Detox iOS E2E Tests Template
on:
workflow_call:
@ -20,10 +20,6 @@ on:
required: false
default: ${{ github.head_ref || github.ref }}
type: string
run-ios-tests:
description: "Run iOS tests"
required: true
type: boolean
run-type:
type: string
required: false
@ -42,12 +38,12 @@ on:
description: "iPhone simulator name"
required: false
type: string
default: "iPhone 15 Pro"
default: "iPhone 16 Pro"
ios_device_os_name:
description: "iPhone simulator OS version"
required: false
type: string
default: "iOS 17.4"
default: "iOS 18.1"
low_bandwidth_mode:
description: "Enable low bandwidth mode"
required: false
@ -84,11 +80,12 @@ env:
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
FAILURE_MESSAGE: "Something has failed"
IOS: "true"
RUNNING_E2E: true
jobs:
generate-specs:
runs-on: ubuntu-22.04
if: ${{ inputs.run-ios-tests }}
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
@ -125,10 +122,9 @@ jobs:
e2e-ios:
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
if: ${{ inputs.run-ios-tests }}
runs-on: macos-14
continue-on-error: true
timeout-minutes: ${{ inputs.low_bandwidth_mode && 80 || 40 }}
timeout-minutes: ${{ inputs.low_bandwidth_mode && 140 || 70 }}
env:
IOS: true
needs:
@ -185,7 +181,7 @@ jobs:
test_server_url: ${{ env.SITE_1_URL }}
- name: Run Detox E2E Tests
continue-on-error: true # Label might have been removed manually
continue-on-error: true # We want to run all the tests
run: |
cd detox
npm run detox:config-gen
@ -224,7 +220,6 @@ jobs:
generate-report:
runs-on: ubuntu-22.04
if: ${{ inputs.run-ios-tests}}
needs:
- generate-specs
- e2e-ios
@ -241,7 +236,7 @@ jobs:
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Download All Artifacts
- name: Download iOS Artifacts
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
path: detox/artifacts/
@ -262,7 +257,6 @@ jobs:
env:
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
IOS: ${{ inputs.run-ios-tests }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
REPORT_PATH: ${{ steps.s3.outputs.path }}
## These are needed for the MM Webhook report

View file

@ -280,6 +280,7 @@ const FloatingTextChipsInput = forwardRef<Ref, Props>(({
key={chipValue}
id={chipValue}
text={chipValue}
testID={`${testID}.${chipValue}`}
onRemove={onChipRemove}
containerStyle={styles.chipContainer}
/>

View file

@ -214,7 +214,7 @@ const OptionItem = ({
value={selected}
trackColor={trackColor}
thumbColor={thumbColor}
testID={`${testID}.toggled.${selected}.${value}`}
testID={`${testID}.toggled.${selected}.button`}
/>
);
} else if (type === OptionType.ARROW) {

View file

@ -37,7 +37,7 @@
"android.emulator": {
"type": "android.emulator",
"device": {
"avdName": "detox_pixel_4_xl_api_31"
"avdName": "detox_pixel_4_xl_api_34"
}
}
},

View file

@ -1,36 +1,49 @@
# How to Run Detox Tests
## Android
This guide will help you set up and run Detox tests for your project.
### Install Dependencies
## Install Dependencies
From the root directory, run the following command to install the necessary dependencies:
First, navigate to the root directory of your project and install the necessary dependencies by running:
```sh
npm install
```
### Inject Detox Settings
navigate to the `detox` folder and run `npm install`
To inject the Detox settings into your project, navigate to the `detox` directory and run the following command:
## Android
### Build Detox Android App
To build the Detox Android app, navigate to the `detox` folder and run:
```sh
npm run inject-detox-settings
```
### Update `minSdkVersion` for `react-native-image-picker`
On macOS machines, update the `minSdkVersion` of `react-native-image-picker` to 23 by running the following command from the root directory.
This is required for Detox to build the test apk targeting android API 31 or higher.
```sh
sed -i '' 's/minSdkVersion 21/minSdkVersion 23/' ./node_modules/react-native-image-picker/android/build.gradle
```
### Build detox android app
From the `detox` folder run:
```
npm run e2e:android-build
```
### Run Detox Android Tests
To execute the Detox tests on Android, navigate to the `detox` folder and run:
```sh
npm run e2e:android-test
```
## iOS
### Build iOS Simulator
To build the iOS simulator for Detox, navigate to the `detox` folder and run:
```sh
npm run e2e:ios-build
```
### Run iOS Tests
To execute the Detox tests on iOS, navigate to the `detox` folder and run:
```sh
npm run e2e:ios-test
```

View file

@ -1,39 +1,132 @@
#!/bin/bash
# Reference: Download Android (AOSP) Emulators - https://github.com/wix/Detox/blob/master/docs/guide/android-dev-env.md#android-aosp-emulators
# sdkmanager "system-images;android-31;default;arm64-v8a"
# sdkmanager --licenses
set -ex
set -o pipefail
SDK_VERSION=31
NAME="detox_pixel_4_xl_api_${SDK_VERSION}"
SDK_VERSION=${1:-34} # First argument is SDK version
AVD_BASE_NAME=${2:-"detox_pixel_4_xl_api_34"} # Second argument is AVD base name
AVD_NAME="${AVD_BASE_NAME}_api_${SDK_VERSION}"
TEST_FILES=${@:3} # Capture all remaining arguments as Detox test files
if emulator -list-avds | grep -q $NAME; then
echo "'${NAME}' Android virtual device already exists."
else
CPU_ARCH_FAMILY=''
CPU_ARCH=''
setup_avd_home() {
if [[ "$CI" == "true" ]]; then
export ANDROID_AVD_HOME=$(pwd)/.android/avd
mkdir -p "$ANDROID_AVD_HOME"
fi
}
get_cpu_architecture() {
if [[ $(uname -p) == 'arm' ]]; then
CPU_ARCH_FAMILY=arm64-v8a
CPU_ARCH=arm64
echo "arm64-v8a arm64"
else
CPU_ARCH_FAMILY=x86_64
CPU_ARCH=x86_64
echo "x86_64 x86_64"
fi
}
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'
cp -r android_emulator/ "$AVD_NAME/"
sed -i -e "s|AvdId = change_avd_id|AvdId = ${AVD_NAME}|g" "$AVD_NAME/config.ini"
sed -i -e "s|avd.ini.displayname = change_avd_displayname|avd.ini.displayname = Detox Pixel 4 XL API ${SDK_VERSION}|g" "$AVD_NAME/config.ini"
sed -i -e "s|abi.type = change_type|abi.type = ${cpu_arch_family}|g" "$AVD_NAME/config.ini"
sed -i -e "s|hw.cpu.arch = change_cpu_arch|hw.cpu.arch = ${cpu_arch}|g" "$AVD_NAME/config.ini"
sed -i -e "s|image.sysdir.1 = change_to_image_sysdir/|image.sysdir.1 = system-images/android-${SDK_VERSION}/default/${cpu_arch_family}/|g" "$AVD_NAME/config.ini"
sed -i -e "s|skin.path = change_to_absolute_path/pixel_4_xl_skin|skin.path = $(pwd)/${AVD_NAME}/pixel_4_xl_skin|g" "$AVD_NAME/config.ini"
echo "hw.cpu.ncore=5" >> "$AVD_NAME/config.ini"
echo "Android virtual device successfully created: ${AVD_NAME}"
}
start_adb_server() {
echo "Restarting ADB server..."
adb kill-server
adb start-server
}
start_emulator() {
echo "Starting the emulator..."
local emulator_opts="-avd $AVD_NAME -no-snapshot -no-boot-anim -no-audio -no-window"
if [[ "$CI" == "true" || "$(uname -s)" == "Linux" ]]; then
emulator $emulator_opts -gpu host -accel on -qemu -m 4096 &
else
emulator $emulator_opts -gpu guest -verbose -qemu -vnc :0
fi
}
wait_for_emulator() {
if [[ "$CI" != "true" ]]; then return; fi
echo "Waiting for emulator to boot..."
adb wait-for-device
until [[ "$(adb shell getprop sys.boot_completed | tr -d '\r')" == "1" ]]; do
echo "Waiting for emulator to fully boot..."
sleep 10
done
echo "Emulator is fully booted."
}
install_app() {
echo "Installing the app..."
adb install -r ../android/app/build/outputs/apk/debug/app-debug.apk
adb shell pm list packages | grep "com.mattermost.rnbeta" && echo "App is installed." || echo "App is not installed."
}
start_server() {
echo "Starting the server..."
cd ..
RUNNING_E2E=true npm run start &
local timeout=120 interval=5 elapsed=0
until nc -z localhost 8081; do
if [[ $elapsed -ge $timeout ]]; then
echo "Server did not start within 3 minutes."
exit 1
fi
echo "Waiting for server to be ready..."
sleep $interval
elapsed=$((elapsed + interval))
done
echo "Server is ready."
}
setup_adb_reverse() {
echo "Setting up ADB reverse port forwarding..."
adb reverse tcp:8081 tcp:8081
}
run_detox_tests() {
echo "Running Detox tests... $@"
cd detox
npm run e2e:android-test -- "$@"
}
main() {
setup_avd_home
if ! emulator -list-avds | grep -q "$AVD_NAME"; then
create_avd
else
echo "'${AVD_NAME}' Android virtual device already exists."
fi
# Create virtual device in a relative "detox_pixel_4_xl_api_${SDK_VERSION}" folder
avdmanager create avd -n $NAME -k "system-images;android-${SDK_VERSION};default;${CPU_ARCH_FAMILY}" -p $NAME -d 'pixel'
start_adb_server
start_emulator
wait_for_emulator
# Copy predefined config and skin
cp -r android_emulator/ $NAME/
sed -i -e "s|AvdId = change_avd_id|AvdId = ${NAME}|g" $NAME/config.ini
sed -i -e "s|avd.ini.displayname = change_avd_displayname|avd.ini.displayname = Detox Pixel 4 XL API ${SDK_VERSION}|g" $NAME/config.ini
sed -i -e "s|abi.type = change_type|abi.type = ${CPU_ARCH_FAMILY}|g" $NAME/config.ini
sed -i -e "s|hw.cpu.arch = change_cpu_arch|hw.cpu.arch = ${CPU_ARCH}|g" $NAME/config.ini
sed -i -e "s|image.sysdir.1 = change_to_image_sysdir/|image.sysdir.1 = system-images/android-${SDK_VERSION}/default/${CPU_ARCH_FAMILY}/|g" $NAME/config.ini
sed -i -e "s|skin.path = change_to_absolute_path/pixel_4_xl_skin|skin.path = $(pwd)/${NAME}/pixel_4_xl_skin|g" $NAME/config.ini
if [[ "$CI" == "true" ]]; then
install_app
start_server
setup_adb_reverse
fi
echo "Android virtual device successfully created: ${NAME}"
fi
run_detox_tests $TEST_FILES
}
main

View file

@ -10,8 +10,8 @@ class AutoResponderNotificationSettingsScreen {
autoResponderNotificationSettingsScreen: 'auto_responder_notification_settings.screen',
backButton: 'screen.back.button',
scrollView: 'auto_responder_notification_settings.scroll_view',
enableAutomaticRepliesOptionToggledOff: 'auto_responder_notification_settings.enable_automatic_replies.option.toggled.false',
enableAutomaticRepliesOptionToggledOn: 'auto_responder_notification_settings.enable_automatic_replies.option.toggled.true',
enableAutomaticRepliesOptionToggledOff: 'auto_responder_notification_settings.enable_automatic_replies.option.toggled.false.button',
enableAutomaticRepliesOptionToggledOn: 'auto_responder_notification_settings.enable_automatic_replies.option.toggled.true.button',
messageInput: 'auto_responder_notification_settings.message.input',
messageInputDescription: 'auto_responder_notification_settings.message.input.description',
};

View file

@ -45,8 +45,8 @@ class ChannelScreen {
postPriorityPicker: 'channel.post_draft.quick_actions.post_priority_action',
postPriorityImportantMessage: 'post_priority_picker_item.important',
postPriorityUrgentMessage: 'post_priority_picker_item.urgent',
postPriorityRequestAck: 'post_priority_picker_item.requested_ack.toggled.false.requested_ack',
postPriorityPersistentNotification: 'post_priority_picker_item.persistent_notifications.toggled.undefined.persistent_notifications',
postPriorityRequestAck: 'post_priority_picker_item.requested_ack.toggled.false.button',
postPriorityPersistentNotification: 'post_priority_picker_item.persistent_notifications.toggled.undefined.button',
};
postPriorityPersistentNotification = element(by.id(this.testID.postPriorityPersistentNotification));

View file

@ -17,8 +17,8 @@ class CreateOrEditChannelScreen {
createButton: 'create_or_edit_channel.create.button',
saveButton: 'create_or_edit_channel.save.button',
scrollView: 'create_or_edit_channel.scroll_view',
makePrivateToggledOff: 'channel_info_form.make_private.toggled.false',
makePrivateToggledOn: 'channel_info_form.make_private.toggled.true',
makePrivateToggledOff: 'channel_info_form.make_private.toggled.false.button',
makePrivateToggledOn: 'channel_info_form.make_private.toggled.true.button',
makePrivateDescription: 'channel_info_form.make_private.description',
displayNameInput: 'channel_info_form.display_name.input',
purposeInput: 'channel_info_form.purpose.input',

View file

@ -18,8 +18,8 @@ class EmailNotificationSettingsScreen {
everyHourOptionSelected: 'email_notification_settings.every_hour.option.selected',
neverOption: 'email_notification_settings.never.option',
neverOptionSelected: 'email_notification_settings.never.option.selected',
emailThreadsOptionToggledOff: 'email_notification_settings.email_threads.option.toggled.false',
emailThreadsOptionToggledOn: 'email_notification_settings.email_threads.option.toggled.true',
emailThreadsOptionToggledOff: 'email_notification_settings.email_threads.option.toggled.false.button',
emailThreadsOptionToggledOn: 'email_notification_settings.email_threads.option.toggled.true.button',
};
emailNotificationSettingsScreen = element(by.id(this.testID.emailNotificationSettingsScreen));

View file

@ -64,7 +64,7 @@ class InviteScreen {
};
getSearchListUserItemText = (id: string) => {
return element(by.id(`${this.testID.searchListUserItemPrefix}.${id}.username`));
return element(by.id(`${this.testID.searchListUserItemPrefix}.${id}.display_name`));
};
getSearchListNoResults = (id: string) => {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ServerScreen} from '@support/ui/screen';
import {ChannelListScreen, ServerScreen} from '@support/ui/screen';
import {timeouts, wait} from '@support/utils';
import {expect} from 'detox';
@ -68,7 +68,7 @@ class LoginScreen {
await element(by.text(/^Log In to Your Account*$/)).tap();
await this.signinButton.tap();
await wait(timeouts.FOUR_SEC);
await waitFor(ChannelListScreen.channelListScreen).toExist().withTimeout(timeouts.TEN_SEC);
};
loginAsAdmin = async (user: any = {}) => {
@ -79,7 +79,8 @@ class LoginScreen {
await this.passwordInput.tap();
await this.passwordInput.replaceText(user.password);
await this.signinButton.tap();
await wait(timeouts.FOUR_SEC);
await waitFor(ChannelListScreen.channelListScreen).toExist().withTimeout(timeouts.TEN_SEC);
};
}

View file

@ -10,12 +10,12 @@ class MentionNotificationSettingsScreen {
mentionNotificationSettingsScreen: 'mention_notification_settings.screen',
backButton: 'screen.back.button',
scrollView: 'mention_notification_settings.scroll_view',
caseSensitiveFirstNameOptionToggledOff: 'mention_notification_settings.case_sensitive_first_name.option.toggled.false',
caseSensitiveFirstNameOptionToggledOn: 'mention_notification_settings.case_sensitive_first_name.option.toggled.true',
nonCaseSensitiveUsernameOptionToggledOff: 'mention_notification_settings.non_case_sensitive_username.option.toggled.false',
nonCaseSensitiveUsernameOptionToggledOn: 'mention_notification_settings.non_case_sensitive_username.option.toggled.true',
channelWideMentionsOptionToggledOff: 'mention_notification_settings.channel_wide_mentions.option.toggled.false',
channelWideMentionsOptionToggledOn: 'mention_notification_settings.channel_wide_mentions.option.toggled.true',
caseSensitiveFirstNameOptionToggledOff: 'mention_notification_settings.case_sensitive_first_name.option.toggled.false.button',
caseSensitiveFirstNameOptionToggledOn: 'mention_notification_settings.case_sensitive_first_name.option.toggled.true.button',
nonCaseSensitiveUsernameOptionToggledOff: 'mention_notification_settings.non_case_sensitive_username.option.toggled.false.button',
nonCaseSensitiveUsernameOptionToggledOn: 'mention_notification_settings.non_case_sensitive_username.option.toggled.true.button',
channelWideMentionsOptionToggledOff: 'mention_notification_settings.channel_wide_mentions.option.toggled.false.button',
channelWideMentionsOptionToggledOn: 'mention_notification_settings.channel_wide_mentions.option.toggled.true.button',
keywordsInput: 'mention_notification_settings.keywords.input',
keywordsInputDescription: 'mention_notification_settings.keywords.input.description',
threadsStartParticipateOption: 'mention_notification_settings.threads_start_participate.option',
@ -62,6 +62,10 @@ class MentionNotificationSettingsScreen {
await expect(this.mentionNotificationSettingsScreen).not.toBeVisible();
};
getKeywordTriggerElement = async (keyword: string) => {
return element(by.text(`${keyword.replace(/ /g, '').toLowerCase()}`));
};
toggleCaseSensitiveFirstNameOptionOn = async () => {
await this.caseSensitiveFirstNameOptionToggledOff.tap();
await expect(this.caseSensitiveFirstNameOptionToggledOn).toBeVisible();

View file

@ -16,8 +16,8 @@ class PushNotificationSettingsScreen {
mentionsOnlyOptionSelected: 'push_notification_settings.mentions_only.option.selected',
nothingOption: 'push_notification_settings.nothing.option',
nothingOptionSelected: 'push_notification_settings.nothing.option.selected',
pushThreadsFollowingOptionToggledOff: 'push_notification_settings.push_threads_following.option.toggled.false',
pushThreadsFollowingOptionToggledOn: 'push_notification_settings.push_threads_following.option.toggled.true',
pushThreadsFollowingOptionToggledOff: 'push_notification_settings.push_threads_following.option.toggled.false.button',
pushThreadsFollowingOptionToggledOn: 'push_notification_settings.push_threads_following.option.toggled.true.button',
mobileOnlineOption: 'push_notification_settings.mobile_online.option',
mobileOnlineOptionSelected: 'push_notification_settings.mobile_online.option.selected',
mobileAwayOption: 'push_notification_settings.mobile_away.option',

View file

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

View file

@ -13,7 +13,7 @@ class TeamDropdownMenuScreen {
teamDropdownMenuScreen = element(by.id(this.testID.teamDropdownMenuScreen));
getTeamIcon = (teamId: string) => {
return element(by.id(`team_sidebar.team_list.team_list_item.${teamId}.team_icon`));
return element(by.id(`team_sidebar.team_list.team_list_item.${teamId}.team_display_name`));
};
getTeamDisplayName = (teamId: string) => {

View file

@ -10,8 +10,8 @@ class TimezoneDisplaySettingsScreen {
timezoneDisplaySettingsScreen: 'timezone_display_settings.screen',
backButton: 'screen.back.button',
scrollView: 'timezone_display_settings.scroll_view',
automaticOptionToggledOff: 'timezone_display_settings.automatic.option.toggled.false',
automaticOptionToggledOn: 'timezone_display_settings.automatic.option.toggled.true',
automaticOptionToggledOff: 'timezone_display_settings.automatic.option.toggled.false.button',
automaticOptionToggledOn: 'timezone_display_settings.automatic.option.toggled.true.button',
manualOption: 'timezone_display_settings.manual.option',
manualOptionInfo: 'timezone_display_settings.manual.option.info',
};

View file

@ -172,13 +172,11 @@ describe('Account - Custom Status', () => {
// # Open custom status screen
await CustomStatusScreen.open();
await wait(timeouts.ONE_SEC);
// * Verify custom status is cleared from input field
await expect(CustomStatusScreen.getCustomStatusEmoji('default')).toBeVisible();
if (isIos()) {
await expect(CustomStatusScreen.statusInput).toHaveValue(defaultStatusText);
} else {
await expect(CustomStatusScreen.statusInput).toHaveText('');
}
await expect(CustomStatusScreen.statusInput).toHaveText('');
// # Go back to account screen
await CustomStatusScreen.close();

View file

@ -66,11 +66,13 @@ describe('Account - Settings - Mention Notification Settings', () => {
it('MM-T5107_2 - should be able to change mention notification settings and save by tapping navigation back button', async () => {
// # Switch toggles, type in keywords as camelcase with spaces, tap on back button, and go back to mention notifications screen
const keywords = ` Keywords ${getRandomId()} `;
const keywords = `Keywords ${getRandomId()}`;
const commasSeparator = ',';
await MentionNotificationSettingsScreen.toggleCaseSensitiveFirstNameOptionOn();
await MentionNotificationSettingsScreen.toggleNonCaseSensitiveUsernameOptionOn();
await MentionNotificationSettingsScreen.toggleChannelWideMentionsOptionOff();
await MentionNotificationSettingsScreen.keywordsInput.replaceText(keywords);
await MentionNotificationSettingsScreen.keywordsInput.typeText(keywords);
await MentionNotificationSettingsScreen.keywordsInput.typeText(commasSeparator);
await MentionNotificationSettingsScreen.back();
await MentionNotificationSettingsScreen.open();
@ -79,7 +81,8 @@ describe('Account - Settings - Mention Notification Settings', () => {
await expect(MentionNotificationSettingsScreen.nonCaseSensitiveUsernameOptionToggledOn).toBeVisible();
await expect(MentionNotificationSettingsScreen.channelWideMentionsOptionToggledOff).toBeVisible();
if (isIos()) {
await expect(MentionNotificationSettingsScreen.keywordsInput).toHaveValue(keywords.replace(/ /g, '').toLowerCase());
const triggerMentionKeyword = await MentionNotificationSettingsScreen.getKeywordTriggerElement(keywords);
await expect(triggerMentionKeyword).toHaveText(keywords.replace(/ /g, '').toLowerCase());
} else {
await expect(MentionNotificationSettingsScreen.keywordsInput).toHaveText(keywords.replace(/ /g, '').toLowerCase());
}

View file

@ -316,9 +316,9 @@ describe('Autocomplete - At-Mention', () => {
await ChannelScreen.postInput.typeText(testUser.username);
// * Verify at-mention autocomplete contains current user
const {atMentionItemUserDisplayName, atMentionItemCurrentUserIndicator, atMentionItemUsername} = Autocomplete.getAtMentionItem(testUser.id);
await expect(atMentionItemUserDisplayName).toHaveText(`${testUser.first_name} ${testUser.last_name}`);
await expect(atMentionItemCurrentUserIndicator).toHaveText(' (you)');
await expect(atMentionItemUsername).toHaveText(` @${testUser.username}`);
await wait(timeouts.TWO_SEC);
const {atMentionItemUserDisplayName, atMentionItemProfilePicture} = Autocomplete.getAtMentionItem(testUser.id);
await expect(atMentionItemUserDisplayName).toBeVisible();
await expect(atMentionItemProfilePicture).toBeVisible();
});
});

View file

@ -253,6 +253,6 @@ describe('Autocomplete - Channel Mention', () => {
// * Verify channel mention autocomplete contains current channel
const {channelMentionItemChannelDisplayName} = Autocomplete.getChannelMentionItem(testChannel.name);
await expect(channelMentionItemChannelDisplayName).toHaveText(`${testChannel.display_name} ~${testChannel.name}`);
await expect(channelMentionItemChannelDisplayName).toBeVisible();
});
});

View file

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

View file

@ -69,10 +69,14 @@ describe('Channels - Channel List', () => {
await expect(ChannelListScreen.headerPlusButton).toBeVisible();
await expect(ChannelListScreen.threadsButton).toBeVisible();
await expect(ChannelListScreen.getCategoryHeaderDisplayName(channelsCategory)).toHaveText('CHANNELS');
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name)).toHaveText(testChannel.display_name);
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, offTopicChannelName)).toHaveText('Off-Topic');
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, townSquareChannelName)).toHaveText('Town Square');
await expect(ChannelListScreen.getCategoryHeaderDisplayName(directMessagesCategory)).toHaveText('DIRECT MESSAGES');
await waitFor(ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, testChannel.name)).toBeVisible();
await waitFor(ChannelListScreen.getChannelItemDisplayName(channelsCategory, offTopicChannelName)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, offTopicChannelName)).toBeVisible();
await waitFor(ChannelListScreen.getChannelItemDisplayName(channelsCategory, townSquareChannelName)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(ChannelListScreen.getChannelItemDisplayName(channelsCategory, townSquareChannelName)).toBeVisible();
await waitFor(ChannelListScreen.getCategoryHeaderDisplayName(directMessagesCategory)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(ChannelListScreen.getCategoryHeaderDisplayName(directMessagesCategory)).toBeVisible();
});
it('MM-T4728_2 - should be able to switch between channels', async () => {
@ -108,6 +112,7 @@ describe('Channels - Channel List', () => {
it('MM-T4728_3 - should be able to collapse and expand categories', async () => {
// # Go to a channel to make it active and go back to channel list screen
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postMessage('Test message');
await ChannelScreen.back();
// * Verify on channel list screen
@ -156,8 +161,14 @@ describe('Channels - Channel List', () => {
// * Verify on create direct message screen
await CreateDirectMessageScreen.toBeVisible();
try {
await CreateDirectMessageScreen.closeTutorial();
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to close tutorial:', error);
}
// # Go back to channel list screen
await CreateDirectMessageScreen.closeTutorial();
await CreateDirectMessageScreen.close();
});

View file

@ -100,7 +100,6 @@ describe('Channels - Create Direct Message', () => {
// # Post a message and go back to channel list screen
await ChannelScreen.postMessage('test');
await ChannelScreen.back();
await device.reloadReactNative();
await ChannelListScreen.toBeVisible();
// * Verify direct message channel for the new user is added to direct message list
@ -147,11 +146,7 @@ describe('Channels - Create Direct Message', () => {
// # Post a message and go back to channel list screen
await ChannelScreen.postMessage('test');
await ChannelScreen.back();
await device.reloadReactNative();
await ChannelListScreen.toBeVisible();
// * Verify group message channel for the other two new users is added to direct message list
await expect(element(by.text(groupDisplayName))).toBeVisible();
});
it('MM-T4730_4 - should display empty search state for create direct message', async () => {

View file

@ -130,7 +130,7 @@ describe('Channels - Favorite and Unfavorite Channel', () => {
await CreateDirectMessageScreen.getUserItem(newUser.id).tap();
await CreateDirectMessageScreen.startButton.tap();
await ChannelScreen.postMessage('test');
await device.reloadReactNative();
await ChannelScreen.back();
await ChannelListScreen.getChannelItemDisplayName(directMessagesCategory, directMessageChannel.name).tap();
await ChannelScreen.introFavoriteAction.tap();
await ChannelScreen.back();

View file

@ -175,9 +175,5 @@ 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();
});
});

View file

@ -197,6 +197,7 @@ describe('Search - Saved Messages', () => {
// * Verify saved message is not displayed anymore
const {postListPostItem} = SavedMessagesScreen.getPostListPostItem(savedPost.id, message);
await waitFor(postListPostItem).not.toExist().withTimeout(3000);
await expect(postListPostItem).not.toExist();
// # Go back to channel list screen

View file

@ -96,6 +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 atMentionItem.tap();
await SearchMessagesScreen.searchInput.tapReturnKey();
@ -274,6 +275,7 @@ describe('Search - Search Messages', () => {
await expect(postListPostItem).toBeVisible();
// # Clear search input, remove recent search item, and go back to channel list screen
await SearchMessagesScreen.searchInput.tap();
await SearchMessagesScreen.searchClearButton.tap();
await SearchMessagesScreen.getRecentSearchItemRemoveButton(searchTerm).tap();
await ChannelListScreen.open();
@ -306,8 +308,8 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.teamPickerButton.tap();
await TeamDropdownMenuScreen.getTeamIcon(testTeamTwo.id).tap();
// * Verify team picker button displays second team icon
await expect(SearchMessagesScreen.getTeamPickerIcon(testTeamTwo.id)).toBeVisible();
// * Verify team picker button displays second team name
await expect(element(by.text(testTeamTwo.display_name))).toBeVisible();
// # Type in a search term that will yield results for second team and tap on search key
await SearchMessagesScreen.searchInput.typeText(searchTerm);
@ -321,8 +323,7 @@ describe('Search - Search Messages', () => {
await SearchMessagesScreen.teamPickerButton.tap();
await TeamDropdownMenuScreen.getTeamIcon(testTeam.id).tap();
// * Verify team picker button displays first team icon and search results do not contain searched message
await expect(SearchMessagesScreen.getTeamPickerIcon(testTeam.id)).toBeVisible();
// * Verify search results do not contain searched message
await expect(postListPostItem).not.toBeVisible();
// # Clear search input, remove recent search item, and go back to channel list screen

View file

@ -77,13 +77,11 @@ describe('Server Login - Connect to Server', () => {
await wait(timeouts.ONE_SEC);
// * Verify invalid url error
await waitFor(serverUrlInputError).toExist().withTimeout(timeouts.TEN_SEC);
await expect(serverUrlInputError).toHaveText('Cannot connect to the server.');
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.');
});
it('MM-T4676_4 - should show connection error on invalid ssl or invalid host', async () => {
await device.reloadReactNative();
// # Connect with invalid ssl and non-empty server display name
const expiredServerUrl = 'expired.badssl.com';
const wrongHostServerUrl = 'wrong.host.badssl.com';
@ -95,19 +93,8 @@ describe('Server Login - Connect to Server', () => {
await wait(timeouts.ONE_SEC);
// * Verify invalid SSL cert error
await waitFor(Alert.invalidSslCertTitle).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.okButton.tap();
// # Connect with invalid host and valid server display name
await device.reloadReactNative();
await serverUrlInput.replaceText(wrongHostServerUrl);
await serverDisplayNameInput.replaceText('Server 1');
await connectButton.tap();
await wait(timeouts.ONE_SEC);
// * Verify invalid SSL cert error
await waitFor(Alert.invalidSslCertTitle).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.okButton.tap();
await waitFor(serverUrlInputError).toExist().withTimeout(timeouts.FOUR_SEC);
await expect(serverUrlInputError).toBeVisible();
});
it('MM-T4676_5 - should show login screen on successful connection to server', async () => {
@ -117,7 +104,7 @@ describe('Server Login - Connect to Server', () => {
await connectButton.tap();
await wait(timeouts.ONE_SEC);
if (isIos()) {
if (isIos() && !process.env.CI) {
// # Tap alert okay button
await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.okayButton.tap();

View file

@ -29,7 +29,7 @@ import {
SettingsScreen,
ThemeDisplaySettingsScreen,
} from '@support/ui/screen';
import {getRandomId, isIos, timeouts, wait} from '@support/utils';
import {getRandomId, timeouts, wait} from '@support/utils';
import {expect} from 'detox';
describe('Smoke Test - Account', () => {
@ -131,20 +131,15 @@ describe('Smoke Test - Account', () => {
it('MM-T5114_3 - should be able to set notification settings', async () => {
// # Open settings screen, open notification settings screen, open mention notification settings screen, type in keywords, tap on back button, and go back to mention notification settings screen
const keywords = `${getRandomId()},${getRandomId()}`;
const keywords = `${getRandomId()}`;
await SettingsScreen.open();
await NotificationSettingsScreen.open();
await MentionNotificationSettingsScreen.open();
await MentionNotificationSettingsScreen.keywordsInput.replaceText(keywords);
await MentionNotificationSettingsScreen.keywordsInput.typeText(',');
await MentionNotificationSettingsScreen.back();
await MentionNotificationSettingsScreen.open();
// * Verify keywords are saved
if (isIos()) {
await expect(MentionNotificationSettingsScreen.keywordsInput).toHaveValue(keywords.toLowerCase());
} else {
await expect(MentionNotificationSettingsScreen.keywordsInput).toHaveText(keywords.toLowerCase());
}
await expect(element(by.text(keywords))).toBeVisible();
// # Go back to notification settings screen, open push notification settings screen, tap on mentions only option, tap on mobile away option, tap on back button, and go back to notification settings screen
await MentionNotificationSettingsScreen.back();

View file

@ -186,7 +186,8 @@ describe('Smoke Test - Messaging', () => {
// # Tap on post to open thread and tap on thread overview unsave button
await postListPostItem.tap();
await ThreadScreen.getThreadOverviewUnsaveButton().tap();
await element(by.text(message)).longPress();
await PostOptionsScreen.unsavePostOption.tap();
// * Verify saved text is not displayed on the post pre-header
await expect(channelPostListPostItemPreHeaderText).not.toBeVisible();

View file

@ -101,7 +101,7 @@ describe('Smoke Test - Server Login', () => {
await expect(Alert.logoutTitle(serverTwoDisplayName)).toBeVisible();
// # Tap on logout button
await Alert.logoutButton2.tap();
await Alert.logoutButton.tap();
// * Verify second server is logged out
await ServerListScreen.getServerItemInactive(serverTwoDisplayName).swipe('left');

View file

@ -58,9 +58,11 @@ describe('Smoke Test - Threads', () => {
// # Create a thread and unfollow thread via thread navigation
const parentMessage = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await waitFor(ChannelScreen.postInput).toBeVisible().withTimeout(timeouts.FOUR_SEC);
await ChannelScreen.postMessage(parentMessage);
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.postMessage(`${parentMessage} reply`);
await ThreadScreen.followingButton.tap();

View file

@ -7,7 +7,7 @@
// - Use element testID when selecting an element. Create one if none.
// *******************************************************************
import {Setup, User} from '@support/server_api';
import {Setup, Team, User} from '@support/server_api';
import {
serverOneUrl,
siteOneUrl,
@ -19,32 +19,27 @@ import {
LoginScreen,
ServerScreen,
} from '@support/ui/screen';
import {isIos, timeouts} from '@support/utils';
import {timeouts} from '@support/utils';
import {expect} from 'detox';
function systemDialog(label: string) {
if (isIos()) {
return element(by.label(label)).atIndex(0);
}
return element(by.text(label));
}
describe('Teams - Invite', () => {
const serverOneDisplayName = 'Server 1';
let testTeam: any;
let testUser: any;
let testUser1: any;
let testUser2: any;
let testUser3: any;
beforeAll(async () => {
const {team, user} = await Setup.apiInit(siteOneUrl);
testTeam = team;
testUser = user;
const {user: user1} = await User.apiCreateUser(siteOneUrl, {prefix: 'i'});
testUser1 = user1;
const {user: user2} = await User.apiCreateUser(siteOneUrl);
testUser2 = user2;
await Team.apiAddUserToTeam(siteOneUrl, testUser2.id, testTeam.id);
// # Log in to server
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
@ -79,7 +74,7 @@ describe('Teams - Invite', () => {
await expect(Invite.teamIcon).toBeVisible();
// * Verify default Selection
await waitFor(Invite.screenSelection).toBeVisible().withTimeout(timeouts.TWO_SEC);
await waitFor(Invite.screenSelection).toBeVisible().withTimeout(timeouts.FOUR_SEC);
// * Verify Server data
await expect(Invite.serverDisplayName).toHaveText(serverOneDisplayName);
@ -92,20 +87,6 @@ describe('Teams - Invite', () => {
await expect(Invite.searchBarInput).toBeVisible();
});
it('MM-T5221 - should be able to share a URL invite to the team', async () => {
if (isIos()) {
// # Tap on Share link
await Invite.shareLinkButton.tap();
const dialog = systemDialog(`Join the ${testTeam.display_name} team`);
// * Verify share dialog is open
await expect(dialog).toExist();
// # Close share dialog
await dialog.swipe('down');
} // no support for Android system dialogs by detox yet. See https://github.com/wix/Detox/issues/3227
});
it('MM-T5361 - should show no results item in search list', async () => {
const noUser = 'qwertyuiop';
@ -146,14 +127,12 @@ describe('Teams - Invite', () => {
});
it('MM-T5363 - should be able to send user invite', async () => {
const username = ` @${testUser1.username}`;
// # Search for an existent user
await Invite.searchBarInput.replaceText(testUser1.username);
// * Validate user item in search list
await waitFor(Invite.getSearchListUserItem(testUser1.id)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await expect(Invite.getSearchListUserItemText(testUser1.id)).toHaveText(username);
await expect(Invite.getSearchListUserItemText(testUser1.id)).toHaveText(testUser1.username);
// # Select user item
await Invite.getSearchListUserItem(testUser1.id).tap();
@ -170,23 +149,21 @@ describe('Teams - Invite', () => {
await expect(Invite.getSummaryReportSent()).toBeVisible();
await expect(Invite.getSummaryReportNotSent()).not.toExist();
await expect(Invite.getSummaryReportUserItem(testUser1.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser1.id)).toHaveText(username);
await expect(Invite.getSummaryReportUserItemText(testUser1.id)).toHaveText(testUser1.username);
});
it('MM-T5364 - should not be able to send user invite to user already in team', async () => {
const username = ` @${testUser1.username}`;
// # Search for an existent user already in team
await Invite.searchBarInput.replaceText(testUser1.username);
await Invite.searchBarInput.replaceText(testUser2.username);
// * Validate user item in search list
await waitFor(Invite.getSearchListUserItem(testUser1.id)).toBeVisible().withTimeout(timeouts.TWO_SEC);
await waitFor(Invite.getSearchListUserItem(testUser2.id)).toBeVisible().withTimeout(timeouts.TWO_SEC);
// # Select user item
await Invite.getSearchListUserItem(testUser1.id).tap();
await Invite.getSearchListUserItem(testUser2.id).tap();
// * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser1.id)).toBeVisible();
await expect(Invite.getSelectedItem(testUser2.id)).toBeVisible();
// # Send invitation
await Invite.sendButton.tap();
@ -195,24 +172,25 @@ describe('Teams - Invite', () => {
await expect(Invite.screenSummary).toBeVisible();
await expect(Invite.getSummaryReportSent()).not.toExist();
await expect(Invite.getSummaryReportNotSent()).toBeVisible();
await expect(Invite.getSummaryReportUserItem(testUser1.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser1.id)).toHaveText(username);
await expect(Invite.getSummaryReportUserItem(testUser2.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser2.id)).toHaveText(testUser2.username);
});
it('MM-T5365 - should handle both sent and not sent invites', async () => {
const {user: testUser2} = await User.apiCreateUser(siteOneUrl, {prefix: 'i'});
const {user: user3} = await User.apiCreateUser(siteOneUrl, {prefix: 'i'});
testUser3 = user3;
// # Search for an existent user
await Invite.searchBarInput.replaceText(testUser2.username);
await Invite.searchBarInput.replaceText(testUser3.username);
// * Validate user item in search list
await waitFor(Invite.getSearchListUserItem(testUser2.id)).toBeVisible().withTimeout(timeouts.TEN_SEC);
await waitFor(Invite.getSearchListUserItem(testUser3.id)).toBeVisible().withTimeout(timeouts.TEN_SEC);
// # Select user item
await Invite.getSearchListUserItem(testUser2.id).tap();
await Invite.getSearchListUserItem(testUser3.id).tap();
// * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser2.id)).toBeVisible();
await expect(Invite.getSelectedItem(testUser3.id)).toBeVisible();
// # Search for a existent user already in team
await Invite.searchBarInput.replaceText(testUser.username);
@ -239,7 +217,7 @@ describe('Teams - Invite', () => {
// * Validate summary report sent
waitFor(Invite.getSummaryReportSent()).toBeVisible();
await expect(Invite.getSummaryReportUserItem(testUser2.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser2.id)).toBeVisible(testUser2.username1);
await expect(Invite.getSummaryReportUserItem(testUser3.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser3.id)).toBeVisible(testUser3.username1);
});
});

View file

@ -1,72 +0,0 @@
// 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');
// Paths to files
const androidManifestPath = path.resolve(
__dirname,
'../android/app/src/debug/AndroidManifest.xml',
);
const settingsGradlePath = path.resolve(__dirname, '../android/settings.gradle');
// Detox code to add to settings.gradle
const detoxSettings = `
include ':detox'
project(':detox').projectDir = new File(rootProject.projectDir, '../detox/node_modules/detox/android')
`;
// Updated AndroidManifest.xml content
const updatedManifest = `<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mattermost.rnbeta">
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
android:exported="true"
tools:node="replace"/>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
android:exported="true"
tools:node="replace"/>
<activity
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity"
android:exported="true"
tools:node="replace"/>
</application>
</manifest>`;
// Update AndroidManifest.xml
function updateAndroidManifest() {
try {
fs.writeFileSync(androidManifestPath, updatedManifest, 'utf-8');
console.log('AndroidManifest.xml updated successfully.');
} catch (err) {
console.error(`Failed to update AndroidManifest.xml: ${err.message}`);
}
}
// Update settings.gradle
function updateSettingsGradle() {
try {
const content = fs.readFileSync(settingsGradlePath, 'utf-8');
if (content.includes("include ':detox'")) {
console.log('Detox settings already present in settings.gradle.');
return;
}
fs.writeFileSync(settingsGradlePath, content + detoxSettings, 'utf-8');
console.log('settings.gradle updated successfully.');
} catch (err) {
console.error(`Failed to update settings.gradle: ${err.message}`);
}
}
// Run updates
updateAndroidManifest();
updateSettingsGradle();

View file

@ -23,7 +23,7 @@
"babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3",
"deepmerge": "4.3.1",
"detox": "20.28.0",
"detox": "20.33.0",
"form-data": "4.0.1",
"jest": "29.7.0",
"jest-circus": "29.7.0",
@ -5232,12 +5232,11 @@
}
},
"node_modules/detox": {
"version": "20.28.0",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.28.0.tgz",
"integrity": "sha512-JeUkWNnYE7lqby3S9AeYJP3ttCBKH+qZWACjWXwvSbe3tm6JeXvecVUYkzSoNfC4IzTX5p+rWvG0IPsfOsZSFw==",
"version": "20.33.0",
"resolved": "https://registry.npmjs.org/detox/-/detox-20.33.0.tgz",
"integrity": "sha512-9KZC3NgCav/IvuWm/EaKNIL5lV2bJf23PPK67OHS2dMhJsDsNBRWP9k3RN7hzLzzUwAXMMoOmk+S1OpOsazNFA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.6.3",
"bunyan": "^1.8.12",
@ -5245,7 +5244,7 @@
"caf": "^15.0.1",
"chalk": "^4.0.0",
"child-process-promise": "^2.2.0",
"detox-copilot": "^0.0.24",
"detox-copilot": "^0.0.27",
"execa": "^5.1.1",
"find-up": "^5.0.0",
"fs-extra": "^11.0.0",
@ -5292,11 +5291,11 @@
}
},
"node_modules/detox-copilot": {
"version": "0.0.24",
"resolved": "https://registry.npmjs.org/detox-copilot/-/detox-copilot-0.0.24.tgz",
"integrity": "sha512-42g0QyJS31URl28YRxc4hGozSXhbbB1sKwzxEjZR9WtLoSx6WYDsQkQD8+yP5t1NExiSCZAfvNmBw8PYQwDKwg==",
"dev": true,
"license": "MIT"
"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",

View file

@ -21,7 +21,7 @@
"babel-plugin-module-resolver": "5.0.2",
"client-oauth2": "4.3.3",
"deepmerge": "4.3.1",
"detox": "20.28.0",
"detox": "20.33.0",
"form-data": "4.0.1",
"jest": "29.7.0",
"jest-circus": "29.7.0",
@ -42,7 +42,6 @@
"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",

View file

@ -99,12 +99,13 @@ const saveReport = async () => {
// Merge all XML reports into one single XML report
const platform = process.env.IOS === 'true' ? 'ios' : 'android';
const combinedFilePath = `${ARTIFACTS_DIR}/${platform}-combined.xml`;
await mergeFiles(path.join(__dirname, combinedFilePath), [`${ARTIFACTS_DIR}/${platform}-results*/${platform}-junit*.xml`]);
console.log(`Merged, check ${combinedFilePath}`);
// Read XML from a file
const xml = fse.readFileSync(combinedFilePath);
const {testsuites} = convertXmlToJson(xml);
const {testsuites} = convertXmlToJson(xml, platform);
// Generate short summary, write to file and then send report via webhook
const allTests = getAllTests(testsuites);

View file

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

View file

@ -11,8 +11,7 @@ const {ARTIFACTS_DIR} = require('./constants');
const MAX_FAILED_TITLES = 5;
function convertXmlToJson(xml) {
const platform = process.env.IOS === 'true' ? 'ios' : 'android';
function convertXmlToJson(xml, platform) {
const jsonFile = `${ARTIFACTS_DIR}/${platform}-junit.json`;
// Convert XML to JSON
@ -27,7 +26,6 @@ function convertXmlToJson(xml) {
// Save JSON in a file
fse.writeFileSync(jsonFile, json);
});
return readJsonFromFile(jsonFile);
}