Add workflow to run detox iOS tests (#7878)

This commit is contained in:
yasserfaraazkhan 2024-06-04 22:42:58 +05:30 committed by GitHub
parent cf254f8a86
commit f46ddb49d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1896 additions and 9238 deletions

View file

@ -27,6 +27,7 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
- [ ] Has UI changes
- [ ] Includes text changes and localization file updates
- [ ] Have tested against the 5 core themes to ensure consistency between them.
- [ ] Have run E2E tests by adding label `E2E iOS tests for PR`.
#### Device Information
This PR was tested on: <!-- Device name(s), OS version(s) -->

View file

@ -0,0 +1,39 @@
# Copyright 2024 Mattermost, Inc.
name: "generate-specs"
description: This action used to split Detox integration tests based on the parallelism provided
inputs:
search_path:
description: The path to look for from within the directory
required: true
parallelism:
description: The parallelism for the tests
required: true
device_name:
description: The name of Device used for the tests
required: false
default: "iPhone 15"
device_os_version:
description: The os of the device used for the tests
required: false
default: "iOS 17.1"
outputs:
specs:
description: The specs generated for the strategy
value: ${{ steps.generate-specs.outputs.specs }}
runs:
using: "composite"
steps:
- name: ci/generate-specs
id: generate-specs
env:
PARALLELISM: ${{ inputs.parallelism }}
SEARCH_PATH: ${{ inputs.search_path }}
DEVICE_NAME: ${{ inputs.device_name }}
DEVICE_OS_VERSION: ${{ inputs.device_os_version }}
run: |
set -e
node ${{ github.action_path }}/split-tests.js | tee output.json
echo "specs=$(cat output.json)" >> $GITHUB_OUTPUT
shell: bash

View file

@ -0,0 +1,94 @@
const fs = require('fs');
const path = require('path');
class DeviceInfo {
constructor(deviceName, deviceOsVersion) {
this.deviceName = deviceName;
this.deviceOsVersion = deviceOsVersion;
}
}
class SpecGroup {
constructor(runId, specs, deviceInfo) {
this.runId = runId;
this.specs = specs;
this.deviceName = deviceInfo.deviceName;
this.deviceOsVersion = deviceInfo.deviceOsVersion;
}
}
class Specs {
constructor(searchPath, parallelism, deviceInfo) {
this.searchPath = searchPath;
this.parallelism = parallelism;
this.rawFiles = [];
this.groupedFiles = [];
this.deviceInfo = deviceInfo;
}
findFiles() {
const dirPath = path.join(this.searchPath);
const fileRegex = /\.e2e\.ts$/;
const walkSync = (currentPath) => {
const files = fs.readdirSync(currentPath);
files.forEach((file) => {
const filePath = path.join(currentPath, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
walkSync(filePath);
} else if (fileRegex.test(filePath)) {
const relativeFilePath = filePath.replace(dirPath + '/', '');
const fullPath = path.join(this.searchPath, relativeFilePath);
this.rawFiles.push(fullPath);
}
});
};
walkSync(dirPath);
}
generateSplits() {
const chunkSize = Math.ceil(this.rawFiles.length / this.parallelism);
let runNo = 1;
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(' ');
const specFileGroup = new SpecGroup(runNo.toString(), fileGroup, this.deviceInfo);
this.groupedFiles.push(specFileGroup);
if (end === this.rawFiles.length) {
break;
}
runNo++;
}
}
dumpSplits() {
const output = {
include: this.groupedFiles,
};
console.log(JSON.stringify(output));
}
}
function main() {
const searchPath = process.env.SEARCH_PATH;
const parallelism = parseInt(process.env.PARALLELISM, 10);
const deviceName = process.env.DEVICE_NAME;
const deviceOsVersion = process.env.DEVICE_OS_VERSION;
const deviceInfo = new DeviceInfo(deviceName, deviceOsVersion);
const specs = new Specs(searchPath, parallelism, deviceInfo);
specs.findFiles();
specs.generateSplits();
specs.dumpSplits();
}
main();

View file

@ -14,3 +14,11 @@ runs:
npm run ios-gems
npm run pod-install
echo "::endgroup::"
- name: Cache Pods
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-

View file

@ -15,5 +15,13 @@ runs:
echo "::endgroup::"
working-directory: ./fastlane
- name: Cache Ruby gems
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps

View file

@ -5,7 +5,7 @@ runs:
using: composite
steps:
- name: ci/setup-node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: ".nvmrc"
cache: "npm"
@ -21,6 +21,14 @@ runs:
node node_modules/\@sentry/cli/scripts/install.js
echo "::endgroup::"
- name: Cache Node.js modules
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: ci/patch-npm-dependencies
shell: bash
run: |

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
@ -26,7 +26,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
@ -54,7 +54,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-beta-${{ github.run_id }}
path: "*.apk"

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
@ -26,7 +26,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
@ -54,7 +54,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-release-${{ github.run_id }}
path: "*.apk"

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
@ -28,7 +28,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
@ -44,7 +44,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
@ -56,7 +56,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/output-ssh-private-key
shell: bash
@ -93,7 +93,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-beta-${{ github.run_id }}
path: "*.ipa"

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
@ -28,7 +28,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
@ -65,7 +65,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-release-${{ github.run_id }}
path: "*.ipa"
@ -77,7 +77,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
@ -93,7 +93,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip

View file

@ -15,7 +15,7 @@ jobs:
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' || github.event.label.name == 'Build App for Android' }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: ci/test
@ -28,7 +28,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@ -60,7 +60,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-pr-${{ github.run_id }}
path: "*.ipa"
@ -72,7 +72,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@ -94,7 +94,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-pr-${{ github.run_id }}
path: "*.apk"

View file

@ -16,6 +16,6 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test

View file

@ -0,0 +1,300 @@
name: Detox E2E Tests Template
on:
workflow_call:
inputs:
run-ios-tests:
description: 'Run iOS tests'
required: true
type: boolean
run-android-tests:
description: 'Run Android tests'
required: true
type: boolean
remove-pr-label:
description: 'Remove PR label'
required: false
type: boolean
default: false
status_check_context:
type: string
required: true
commit_sha:
type: string
required: true
run-type:
type: string
required: false
default: 'PR'
testcase_failure_fatal:
description: 'Should failures be considered fatal'
required: false
type: boolean
default: false
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.head_ref || github.ref_name }}
COMMIT_HASH: ${{ github.sha }}
DETOX_AWS_S3_BUCKET: 'mattermost-detox-report'
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 }}
HEADLESS: 'true'
TYPE: ${{ inputs.run-type }}
PULL_REQUEST: 'https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}'
SITE_1_URL: ${{ secrets.MM_MOBILE_E2E_SITE_1_URL }}
SITE_2_URL: ${{ secrets.MM_MOBILE_E2E_SITE_2_URL }}
SITE_3_URL: ${{ secrets.MM_MOBILE_E2E_SITE_3_URL }}
ZEPHYR_ENABLE: false
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"
jobs:
update-initial-status:
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: ${{ inputs.commit_sha }}
context: ${{ inputs.status_check_context }}
description: Detox tests for mattermost mobile app
status: pending
generate-specs:
needs: update-initial-status
runs-on: ubuntu-22.04
if: ${{ inputs.run-ios-tests || inputs.run-android-tests }}
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
build_id: ${{ env.BUILD_ID }}
device_name: ${{ env.DEVICE_NAME }}
device_os_version: ${{ env.DEVICE_OS_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set Build ID
id: resolve-device
run: |
cd detox
DEVICE_NAME=$(node -p "require('./.detoxrc').devices['ios.simulator'].device.type")
DEVICE_OS_VERSION=$(node -p "require('./.detoxrc').devices['ios.simulator'].device.os")
BUILD_ID="${{ github.run_id }}-$DEVICE_NAME-$DEVICE_OS_VERSION"
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> $GITHUB_ENV
echo "DEVICE_NAME=$(echo ${DEVICE_NAME})" >> $GITHUB_ENV
echo "DEVICE_OS_VERSION=$(echo ${DEVICE_OS_VERSION})" >> $GITHUB_ENV
- name: Generate Test Specs
id: generate-specs
uses: ./.github/actions/generate-specs
with:
parallelism: 10
search_path: detox/e2e/test
device_name: ${{ env.DEVICE_NAME }}
device_os_version: ${{env.DEVICE_OS_VERSION}}
build-ios-simulator:
needs: update-initial-status
runs-on: macos-14-large
if: ${{ inputs.run-ios-tests }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Prepare iOS Build
uses: ./.github/actions/prepare-ios-build
- name: Build iOS Simulator
env:
TAG: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios simulator --env ios.simulator
working-directory: ./fastlane
- name: Upload iOS Simulator Build
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
e2e-ios:
continue-on-error: true
env:
IOS: true
runs-on: macos-14-large
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
if: ${{ inputs.run-ios-tests }}
timeout-minutes: 40
needs:
- generate-specs
- build-ios-simulator
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Install Homebrew Dependencies
run: |
brew tap wix/brew
brew install applesimutils
- name: Download iOS Simulator Build
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: ios-build-simulator-${{ github.run_id }}
path: mobile-artifacts
- name: Unzip iOS Simulator Build
run: unzip -o mobile-artifacts/*.zip -d mobile-artifacts/
- name: Start React Native Metro Server
run: npm run start &
- name: Install Detox Dependencies
run: cd detox && npm i
- name: Run Detox E2E Tests
continue-on-error: true # Label might have been removed manually
run: cd detox && npm run e2e:ios-test -- ${{ matrix.specs }}
- name: Upload iOS Test Report
if: always()
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-results-${{ matrix.runId }}
path: detox/artifacts/
download-e2e-results:
runs-on: ubuntu-22.04
if: ${{ inputs.run-ios-tests || inputs.run-android-tests }}
needs:
- generate-specs
- e2e-ios
outputs:
failures: "${{ steps.calculate-failures.outputs.failures }}"
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Create artifacts directory
run: mkdir -p detox/artifacts/
- name: Download All Artifacts
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
path: detox/artifacts/
pattern: ios-results-*
- name: Save report Detox Dependencies
id: report-link
run: |
cd detox
npm ci
npm run e2e:save-report
env:
IOS: ${{ inputs.run-ios-tests }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
DEVICE_NAME: ${{ needs.generate-specs.outputs.device_name }}
DEVICE_OS_VERSION: ${{ needs.generate-specs.outputs.device_os_version }}
- name: Calculate failures
id: calculate-failures
run: |
FAILURES=$(find detox/artifacts/ -name 'summary.json' | xargs -l jq -r '.stats.failures' | jq -s add)
echo "failures=${FAILURES}" >> $GITHUB_OUTPUT
echo "Detox run completed with $FAILURES failures"
update-final-status:
runs-on: ubuntu-22.04
needs:
- generate-specs
- download-e2e-results
steps:
- name: Set Target URL
id: set-url
run: |
s3Folder="${{ env.BUILD_ID }}-${{ env.COMMIT_HASH }}-${{ env.BRANCH }}"
s3Folder=$(echo "$s3Folder" | sed 's/\./-/g')
if ${{ inputs.run-ios-tests }}; then
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${s3Folder}/jest-stare/ios-report.html" >> $GITHUB_ENV
else
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${s3Folder}/jest-stare/android-report.html" >> $GITHUB_ENV
fi
env:
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
- name: Determine Status
id: determine-status
run: |
if [[ ${{ needs.download-e2e-results.outputs.failures }} -gt 0 && "${{ inputs.testcase_failure_fatal }}" == "true" ]]; then
echo "status=failure" >> $GITHUB_ENV
else
echo "status=success" >> $GITHUB_ENV
fi
- uses: mattermost/actions/delivery/update-commit-status@main
env:
GITHUB_TOKEN: ${{ github.token }}
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ inputs.commit_sha }}
context: ${{ inputs.status_check_context }}
description: |
Completed with ${{ needs.download-e2e-results.outputs.failures }} failures
status: ${{ env.status }}
target_url: ${{ env.TARGET_URL }}
e2e-remove-label:
if: ${{ inputs.remove-pr-label }}
needs:
- download-e2e-results
runs-on: ubuntu-22.04
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 iosLabel = 'E2E iOS tests for PR';
const androidLabel = 'E2E Android tests for PR';
const labels = context.payload.pull_request.labels.map(label => label.name);
if (labels.includes(iosLabel)) {
github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: iosLabel,
});
}
if (labels.includes(androidLabel)) {
github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: androidLabel,
});
}

94
.github/workflows/e2e-detox.yml vendored Normal file
View file

@ -0,0 +1,94 @@
name: Detox E2E Tests
on:
schedule:
- cron: '0 0 * * *' # every day at midnight
push:
branches:
- 'release-*'
pull_request:
branches:
- 'main'
types:
- labeled
workflow_dispatch:
inputs:
ios_version:
description: 'Enter iOS version'
required: false
default: "iOS 17.4"
type: choice
options:
- "iOS 17.5"
- "iOS 17.4"
- "iOS 17.2"
- "iOS 17.0"
device_name:
description: 'Enter iOS Device Name'
required: false
default: "iPhone 15 Pro"
type: choice
options:
- "iPhone 15 Pro"
- "iPhone 15"
- "iPhone 14 Pro"
- "iPhone 14"
jobs:
run-ios-tests-on-pr:
name: iOS Mobile Tests on PR
uses: ./.github/workflows/e2e-detox-ci-template.yml
if: ${{
(
github.event_name == 'pull_request' &&
github.event.pull_request.labels &&
contains(github.event.pull_request.labels.*.name, 'E2E iOS tests for PR')
)
}}
with:
run-ios-tests: true
run-android-tests: false
remove-pr-label: true
commit_sha: "${{ github.event.pull_request.head.sha || github.sha }}"
status_check_context: "Detox iOS Mobile Tests on PR"
run-type: 'PR'
secrets: inherit
run-ios-tests-on-release:
name: iOS Mobile Tests on Release
uses: ./.github/workflows/e2e-detox-ci-template.yml
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release-') }}
with:
run-ios-tests: true
run-android-tests: false
remove-pr-label: false
commit_sha: "${{ github.sha }}"
status_check_context: "Detox iOS Mobile Tests on Release"
run-type: 'RELEASE'
secrets: inherit
run-ios-tests-on-main-scheduled:
name: iOS Mobile Tests on Main (Scheduled)
uses: ./.github/workflows/e2e-detox-ci-template.yml
if: ${{ github.event_name == 'schedule' && github.ref == 'refs/heads/main' }}
with:
run-ios-tests: true
run-android-tests: false
remove-pr-label: false
commit_sha: "${{ github.sha }}"
status_check_context: "Detox iOS Mobile Tests on Main (Scheduled)"
run-type: 'MAIN'
secrets: inherit
run-ios-tests-on-manual-trigger:
name: iOS Mobile Tests on Manual Trigger
uses: ./.github/workflows/e2e-detox-ci-template.yml
if: ${{ github.event_name == 'workflow_dispatch' }}
with:
run-ios-tests: true
run-android-tests: false
remove-pr-label: false
commit_sha: "${{ github.sha }}"
status_check_context: "Detox iOS Mobile Tests on Manual Trigger"
run-type: 'MANUAL'
secrets: inherit

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
@ -20,7 +20,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
@ -42,7 +42,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-unsigned
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
path: Mattermost-unsigned.ipa
name: Mattermost-unsigned.ipa
@ -53,7 +53,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
@ -68,7 +68,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-unsigned-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
path: Mattermost-unsigned.apk
name: Mattermost-unsigned.apk
@ -80,7 +80,7 @@ jobs:
- build-android-unsigned
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ruby/setup-ruby@22fdc77bf4148f810455b226c90fb81b5cbc00a7 # v1.171.0

View file

@ -8,7 +8,7 @@
"apps": {
"ios.debug": {
"type": "ios.app",
"binaryPath": "../ios/Build/Products/Debug-iphonesimulator/Mattermost.app"
"binaryPath": "../mobile-artifacts/Mattermost.app"
},
"ios.release": {
"type": "ios.app",
@ -30,7 +30,8 @@
"ios.simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 14"
"type": "iPhone 15 Pro",
"os": "iOS 17.4"
}
},
"android.emulator": {

View file

@ -48,7 +48,7 @@ export const apiCreateUser = async (baseUrl: string, {prefix = 'user', user = nu
newUser,
);
return {user: {...response.data, password: newUser.password}};
return {user: {...response.data, newUser}};
} catch (err) {
return getResponseFromError(err);
}

View file

@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const serverOneUrl = process.env.SITE_1_URL || (process.env.IOS === 'true' ? 'http://127.0.0.1:8065' : 'http://10.0.2.2:8065');
export const siteOneUrl = process.env.SITE_1_URL || 'http://127.0.0.1:8065';
export const serverTwoUrl = process.env.SITE_2_URL || 'https://mobile02.test.mattermost.cloud';
export const siteTwoUrl = process.env.SITE_2_URL || 'https://mobile02.test.mattermost.cloud';
export const serverThreeUrl = process.env.SITE_3_URL || 'https://mobile03.test.mattermost.cloud';
export const siteThreeUrl = process.env.SITE_3_URL || 'https://mobile03.test.mattermost.cloud';
export const serverOneUrl = process.env.SITE_1_URL || (process.env.IOS === 'true' ? 'http://localhost:8065' : 'http://10.0.2.2:8065');
export const siteOneUrl = process.env.SITE_1_URL || 'http://localhost:8065';
export const serverTwoUrl = process.env.SITE_2_URL || 'http://localhost:8065';
export const siteTwoUrl = process.env.SITE_2_URL || 'http://localhost:8065';
export const serverThreeUrl = process.env.SITE_3_URL || 'http://localhost:8065';
export const siteThreeUrl = process.env.SITE_3_URL || 'http://localhost:8065';
export const smtpUrl = process.env.SMTP_URL || 'http://127.0.0.1:9001';
export const adminEmail = process.env.ADMIN_EMAIL || 'sysadmin@sample.mattermost.com';
export const adminUsername = process.env.ADMIN_USERNAME || 'sysadmin';

View file

@ -22,6 +22,7 @@ import {expect} from 'detox';
class ChannelScreen {
testID = {
archievedCloseChannelButton: 'channel.post_draft.archived.close_channel.button',
channelScreenPrefix: 'channel.',
channelScreen: 'channel.screen',
channelQuickActionsButton: 'channel_header.channel_quick_actions.button',
@ -43,6 +44,7 @@ class ChannelScreen {
toastMessage: 'toast.message',
};
archievedCloseChannelButton = element(by.id(this.testID.archievedCloseChannelButton));
channelScreen = element(by.id(this.testID.channelScreen));
channelQuickActionsButton = element(by.id(this.testID.channelQuickActionsButton));
favoriteQuickAction = element(by.id(this.testID.favoriteQuickAction));
@ -172,6 +174,7 @@ class ChannelScreen {
postMessage = async (message: string) => {
// # Post message
await this.postInput.tap();
await this.postInput.clearText();
await this.postInput.replaceText(message);
await this.tapSendButton();
};

View file

@ -139,6 +139,8 @@ 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 this.convertPrivateOption.tap({x: 1, y: 1});
const {
@ -165,6 +167,8 @@ class ChannelInfoScreen {
};
leaveChannel = async ({confirm = true} = {}) => {
await this.scrollView.tap({x: 1, y: 1});
await this.scrollView.scroll(200, 'down');
await waitFor(this.leaveChannelOption).toExist().withTimeout(timeouts.TWO_SEC);
if (isAndroid()) {
await this.scrollView.scrollTo('bottom');

View file

@ -46,7 +46,7 @@ class CreateDirectMessageScreen {
};
getUserItem = (userId: string) => {
return element(by.id(`${this.testID.userItemPrefix}${userId}`));
return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}`));
};
getUserItemProfilePicture = (userId: string) => {
@ -54,7 +54,7 @@ class CreateDirectMessageScreen {
};
getUserItemDisplayName = (userId: string) => {
return element(by.id(`${this.testID.userItemPrefix}${userId}.display_name`));
return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}.display_name`));
};
toBeVisible = async () => {

View file

@ -59,6 +59,8 @@ class CreateOrEditChannelScreen {
openEditChannel = async () => {
// # Open edit channel screen
await ChannelInfoScreen.scrollView.tap({x: 1, y: 1});
await ChannelInfoScreen.scrollView.scroll(100, 'down');
await ChannelInfoScreen.editChannelOption.tap();
return this.toBeVisible();

View file

@ -100,7 +100,7 @@ class InviteScreen {
};
getSummaryReportUserItemText = (id: string) => {
return element(by.id(`${this.testID.summaryReportUserItemPrefix}.${id}.username`));
return element(by.id(`${this.testID.summaryReportUserItemPrefix}.${id}.display_name`));
};
toBeVisible = async () => {

View file

@ -60,9 +60,17 @@ class LoginScreen {
login = async (user: any = {}) => {
await this.toBeVisible();
await this.usernameInput.replaceText(user.username);
await this.passwordInput.replaceText(user.password);
await this.signinButton.tap();
await this.usernameInput.typeText(`${user.newUser.email}\n`);
await this.passwordInput.typeText(`${user.newUser.password}\n`);
await wait(timeouts.FOUR_SEC);
};
loginAsAdmin = async (user: any = {}) => {
await this.toBeVisible();
await this.usernameInput.typeText(user.username);
await this.passwordInput.typeText(`${user.password}\n`);
await wait(timeouts.ONE_SEC);
};
}

View file

@ -20,6 +20,8 @@ class ServerScreen {
displayHelp: 'server_form.display_help',
connectButton: 'server_form.connect.button',
connectButtonDisabled: 'server_form.connect.button.disabled',
usernameInput: 'login_form.username.input',
usernameInputError: 'login_form.username.input.error',
};
serverScreen = element(by.id(this.testID.serverScreen));
@ -35,6 +37,7 @@ class ServerScreen {
displayHelp = element(by.id(this.testID.displayHelp));
connectButton = element(by.id(this.testID.connectButton));
connectButtonDisabled = element(by.id(this.testID.connectButtonDisabled));
usernameInput = element(by.id(this.testID.usernameInput));
toBeVisible = async () => {
await waitFor(this.serverScreen).toExist().withTimeout(timeouts.TEN_SEC);
@ -53,13 +56,18 @@ class ServerScreen {
}
if (isIos()) {
await this.tapConnectButton();
if (serverUrl.includes('127.0.0.1')) {
// # Tap alert okay button
await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.okayButton.tap();
if (serverUrl.includes('127.0.0.1') || !process.env.CI) {
try {
// # Tap alert okay button
await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
await Alert.okayButton.tap();
} catch (error) {
/* eslint-disable no-console */
console.log('Alert button did not appear!');
}
}
}
await waitFor(this.usernameInput).toExist().withTimeout(timeouts.ONE_SEC);
};
close = async () => {

View file

@ -69,7 +69,8 @@ describe('Account - Settings - About', () => {
await expect(AboutScreen.databaseSchemaVersionTitle).toHaveText('Database Schema Version:');
await expect(AboutScreen.databaseSchemaVersionValue).toBeVisible();
await expect(AboutScreen.copyInfoButton).toBeVisible();
await expect(AboutScreen.copyInfoButton).toHaveText('Copy info');
await expect(element(by.text(new RegExp('Copy info', 'i')))).toBeVisible();
if (isLicensed) {
await expect(AboutScreen.licensee).toBeVisible();
} else {

View file

@ -26,7 +26,7 @@ describe('Channels - Convert to Private Channel', () => {
beforeAll(async () => {
// # Log in to server as admin
await ServerScreen.connectToServer(siteOneUrl, siteOneDisplayName);
await LoginScreen.login(getAdminAccount());
await LoginScreen.loginAsAdmin(getAdminAccount());
});
beforeEach(async () => {

View file

@ -71,19 +71,13 @@ describe('Channels - Find Channels', () => {
it('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.name);
// * Verify search returns the target public channel item
await wait(timeouts.ONE_SEC);
await expect(FindChannelsScreen.getFilteredChannelItemDisplayName(testChannel.name)).toHaveText(testChannel.display_name);
await FindChannelsScreen.searchInput.replaceText(testChannel.display_name);
// # Tap on the target public channel item
await FindChannelsScreen.getFilteredChannelItem(testChannel.name).tap();
// * Verify on target public channel screen
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(testChannel.display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(testChannel.display_name);
await verifyDetailsOnChannelScreen(testChannel.display_name);
// # Go back to channel list screen
await ChannelScreen.back();
@ -124,10 +118,13 @@ describe('Channels - Find Channels', () => {
// * Verify search returns the target group message channel item
await wait(timeouts.ONE_SEC);
await expect(FindChannelsScreen.getFilteredChannelItemDisplayName(groupMessageChannel.name)).toHaveText(`${testOtherUser1.username}, ${testOtherUser2.username}, sysadmin`);
await FindChannelsScreen.getFilteredChannelItem(groupMessageChannel.name).tap();
// * Verify on target GM screen
await verifyDetailsOnChannelScreen(`${testOtherUser1.username}, admin, ${testOtherUser2.username}`);
// # Go back to channel list screen
await FindChannelsScreen.close();
await ChannelScreen.back();
});
it('MM-T4907_5 - should be able to find an archived channel', async () => {
@ -140,10 +137,14 @@ describe('Channels - Find Channels', () => {
// * Verify search returns the target archived channel item
await wait(timeouts.ONE_SEC);
await expect(FindChannelsScreen.getFilteredChannelItemDisplayName(archivedChannel.name)).toHaveText(archivedChannel.display_name);
await FindChannelsScreen.getFilteredChannelItem(archivedChannel.name).tap();
// # Go back to channel list screen
await FindChannelsScreen.close();
// * Verify on archievd channel name
await verifyDetailsOnChannelScreen(archivedChannel.display_name);
// # Go back to channel list screen by closing archived channel
await expect(ChannelScreen.archievedCloseChannelButton).toBeVisible();
await ChannelScreen.archievedCloseChannelButton.tap();
});
it('MM-T4907_6 - should be able to find a joined private channel and not find an unjoined private channel', async () => {
@ -156,7 +157,6 @@ describe('Channels - Find Channels', () => {
// * Verify search returns the target joined private channel item
await wait(timeouts.ONE_SEC);
await expect(FindChannelsScreen.getFilteredChannelItemDisplayName(joinedPrivateChannel.name)).toHaveText(joinedPrivateChannel.display_name);
// # Search for an unjoined private channel
await FindChannelsScreen.searchInput.replaceText(unjoinedPrivateChannel.name);
@ -169,3 +169,9 @@ describe('Channels - Find Channels', () => {
await FindChannelsScreen.close();
});
});
async function verifyDetailsOnChannelScreen(display_name: string) {
await ChannelScreen.toBeVisible();
await expect(ChannelScreen.headerTitle).toHaveText(display_name);
await expect(ChannelScreen.introDisplayName).toHaveText(display_name);
}

View file

@ -22,12 +22,11 @@ import {expect} from 'detox';
describe('Channels - Unarchive Channel', () => {
const serverOneDisplayName = 'Server 1';
const channelsCategory = 'channels';
beforeAll(async () => {
// # Log in to server as admin
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(getAdminAccount());
await LoginScreen.loginAsAdmin(getAdminAccount());
});
beforeEach(async () => {
@ -43,7 +42,6 @@ describe('Channels - Unarchive Channel', () => {
it('MM-T4944_1 - should be able to unarchive a public channel and confirm', async () => {
// # Create a public channel screen, open channel info screen, and tap on archive channel option and confirm
const channelDisplayName = `Channel ${getRandomId()}`;
const channelName = channelDisplayName.replace(/ /, '-').toLowerCase();
await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName);
await CreateOrEditChannelScreen.createButton.tap();
@ -57,8 +55,6 @@ describe('Channels - Unarchive Channel', () => {
// # Open channel info screen, tap on unarchive channel and confirm, close and re-open app to reload, and re-open unarchived public channel
await ChannelInfoScreen.open();
await ChannelInfoScreen.unarchivePublicChannel({confirm: true});
await device.reloadReactNative();
await ChannelScreen.open(channelsCategory, channelName);
// * Verify on unarchived public channel screen and active post draft is displayed
await ChannelScreen.toBeVisible();
@ -71,7 +67,6 @@ describe('Channels - Unarchive Channel', () => {
it('MM-T4944_2 - should be able to unarchive a private channel and confirm', async () => {
// # Create a private channel screen, open channel info screen, and tap on archive channel option and confirm
const channelDisplayName = `Channel ${getRandomId()}`;
const channelName = channelDisplayName.replace(/ /, '-').toLowerCase();
await CreateOrEditChannelScreen.openCreateChannel();
await CreateOrEditChannelScreen.toggleMakePrivateOn();
await CreateOrEditChannelScreen.displayNameInput.replaceText(channelDisplayName);
@ -86,8 +81,6 @@ describe('Channels - Unarchive Channel', () => {
// # Open channel info screen, tap on unarchive channel and confirm, close and re-open app to reload, and re-open unarchived private channel
await ChannelInfoScreen.open();
await ChannelInfoScreen.unarchivePrivateChannel({confirm: true});
await device.reloadReactNative();
await ChannelScreen.open(channelsCategory, channelName);
// * Verify on unarchived private channel screen and active post draft is displayed
await ChannelScreen.toBeVisible();

View file

@ -24,6 +24,7 @@ import {
PostOptionsScreen,
ReactionsScreen,
ServerScreen,
UserProfileScreen,
} from '@support/ui/screen';
import {getRandomId} from '@support/utils';
import {expect} from 'detox';
@ -115,14 +116,15 @@ describe('Messaging - Emojis and Reactions', () => {
// * Verify user who reacted with the emoji
await ReactionsScreen.toBeVisible();
const {reactorItemEmojiAliases, reactorItemUserProfilePicture, reactorItemUserDisplayName, reactorItemUsername} = ReactionsScreen.getReactorItem(testUser.id, 'fire');
const {reactorItemEmojiAliases, reactorItemUserProfilePicture, reactorItemUser} = ReactionsScreen.getReactorItem(testUser.id, 'fire');
await expect(reactorItemEmojiAliases).toHaveText(':fire:');
await expect(reactorItemUserProfilePicture).toBeVisible();
await expect(reactorItemUserDisplayName).toHaveText(`${testUser.first_name} ${testUser.last_name}`);
await expect(reactorItemUsername).toHaveText(` @${testUser.username}`);
await expect(reactorItemUser).toBeVisible();
await reactorItemUser.tap();
await expect(UserProfileScreen.userDisplayName).toHaveText(`@${testUser.username}`);
// # Go back to channel list screen
await ReactionsScreen.close();
await UserProfileScreen.close();
await ChannelScreen.back();
});

View file

@ -92,6 +92,7 @@ describe('Messaging - Message Draft', () => {
const message = `Message ${getRandomId()}`;
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postInput.tap();
await ChannelScreen.postInput.clearText();
await ChannelScreen.postInput.replaceText(message);
// * Verify message draft exists in post draft
@ -122,6 +123,7 @@ describe('Messaging - Message Draft', () => {
let message = '1234567890'.repeat(1638) + '1234';
await ChannelScreen.open(channelsCategory, testChannel.name);
await ChannelScreen.postInput.tap();
await ChannelScreen.postInput.clearText();
await ChannelScreen.postInput.replaceText(message);
// * Verify warning message is displayed and send button is disabled

View file

@ -60,15 +60,15 @@ 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.replaceText('');
await passwordInput.replaceText('password');
await usernameInput.clearText();
await passwordInput.typeText('password');
// * Verify signin button is disabled
await expect(signinButtonDisabled).toBeVisible();
// # Log in with non-empty username and empty password
await usernameInput.replaceText('username');
await passwordInput.replaceText('');
await usernameInput.typeText('username');
await passwordInput.clearText();
// * Verify signin button is disabled
await expect(signinButtonDisabled).toBeVisible();
@ -76,8 +76,8 @@ 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.replaceText('username');
await passwordInput.replaceText('password');
await usernameInput.typeText('username');
await passwordInput.typeText('password');
await signinButton.tap();
// * Verify incorrect combination error
@ -87,8 +87,8 @@ 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.replaceText(user.username);
await passwordInput.replaceText(user.password);
await usernameInput.typeText(user.newUser.username);
await passwordInput.typeText(user.newUser.password);
await signinButton.tap();
// * Verify on channel list screen and channel list header shows team display name and server display name

View file

@ -202,9 +202,6 @@ describe('Teams - Invite', () => {
it('MM-T5365 - should handle both sent and not sent invites', async () => {
const {user: testUser2} = await User.apiCreateUser(siteOneUrl, {prefix: 'i'});
const username1 = ` @${testUser1.username}`;
const username2 = ` @${testUser2.username}`;
// # Search for an existent user
await Invite.searchBarInput.replaceText(testUser2.username);
@ -218,31 +215,31 @@ describe('Teams - Invite', () => {
await expect(Invite.getSelectedItem(testUser2.id)).toBeVisible();
// # Search for a existent user already in team
await Invite.searchBarInput.replaceText(testUser1.username);
await Invite.searchBarInput.replaceText(testUser.username);
// # Wait for user item in search list
await waitFor(Invite.getSearchListUserItem(testUser1.id)).toExist().withTimeout(timeouts.TWO_SEC);
await waitFor(Invite.getSearchListUserItem(testUser.id)).toExist().withTimeout(timeouts.TWO_SEC);
// # Select user item
await Invite.getSearchListUserItem(testUser1.id).tap();
await Invite.getSearchListUserItem(testUser.id).tap();
// * Validate user is added to selected items
await expect(Invite.getSelectedItem(testUser1.id)).toBeVisible();
await expect(Invite.getSelectedItem(testUser.id)).toBeVisible();
// # Send invitation
await Invite.sendButton.tap();
// * Validate summary
await waitFor(Invite.screenSummary).toBeVisible().withTimeout(timeouts.TEN_SEC);
waitFor(Invite.screenSummary).toBeVisible();
// * Validate summary report not sent
await expect(Invite.getSummaryReportNotSent()).toBeVisible();
await expect(Invite.getSummaryReportUserItem(testUser1.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser1.id)).toHaveText(username1);
await expect(Invite.getSummaryReportUserItem(testUser.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser.id)).toBeVisible(testUser.username1);
// * Validate summary report sent
await waitFor(Invite.getSummaryReportSent()).toBeVisible().withTimeout(timeouts.TEN_SEC);
waitFor(Invite.getSummaryReportSent()).toBeVisible();
await expect(Invite.getSummaryReportUserItem(testUser2.id)).toBeVisible();
await expect(Invite.getSummaryReportUserItemText(testUser2.id)).toHaveText(username2);
await expect(Invite.getSummaryReportUserItemText(testUser2.id)).toBeVisible(testUser2.username1);
});
});

10324
detox/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,13 +14,14 @@
"@types/jest": "29.5.5",
"@types/tough-cookie": "4.0.3",
"@types/uuid": "9.0.4",
"async": "3.2.5",
"axios": "1.5.0",
"axios-cookiejar-support": "4.0.7",
"babel-jest": "29.7.0",
"babel-plugin-module-resolver": "5.0.0",
"client-oauth2": "4.3.3",
"deepmerge": "4.3.1",
"detox": "20.11.4",
"detox": "20.19.3",
"form-data": "4.0.0",
"jest": "29.7.0",
"jest-circus": "29.7.0",
@ -46,11 +47,14 @@
"e2e:android-test": "detox test -c android.emu.debug",
"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-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",
"check": "npm run lint && npm run tsc",
"clean-detox": "detox clean",
"lint": "eslint --ignore-pattern node_modules --quiet .",
"e2e:save-report": "node save_report.js",
"start:webhook": "node webhook_server.js",
"tsc": "NODE_OPTIONS=--max_old_space_size=12000 tsc --noEmit"
}

View file

@ -31,6 +31,7 @@
* - TYPE=[type], e.g. "MASTER", "PR", "RELEASE", "GEKIDOU"
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
@ -74,17 +75,19 @@ const saveReport = async () => {
removeOldGeneratedReports();
const detox_version = shell.exec('npm list detox').stdout.split('\n')[1].split('@')[1].trim();
const headless = IOS === 'true' ? false : HEADLESS === 'true';
const headless = IOS === 'true' ? 'false' : HEADLESS === 'true';
const os_name = os.platform();
const os_version = os.release();
const node_version = process.version;
const npm_version = shell.exec('npm --version').stdout.trim();
const device_name = DEVICE_NAME;
const device_os_version = DEVICE_OS_VERSION;
// Write environment details to file
const environmentDetails = {
detox_version,
device_name: DEVICE_NAME,
device_os_version: DEVICE_OS_VERSION,
device_name,
device_os_version,
headless,
os_name,
os_version,
@ -96,7 +99,7 @@ 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}-junit*.xml`]);
await mergeFiles(path.join(__dirname, combinedFilePath), [`${ARTIFACTS_DIR}/ios-results*/${platform}-junit*.xml`]);
console.log(`Merged, check ${combinedFilePath}`);
// Read XML from a file
@ -112,9 +115,23 @@ const saveReport = async () => {
// Generate jest-stare report
const jestStareOutputDir = path.join(__dirname, `${ARTIFACTS_DIR}/jest-stare`);
const jestStareCombinedFilePath = `${jestStareOutputDir}/${platform}-combined.json`;
await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/jest-stare/${platform}-data*.json`]);
if (!fs.existsSync(jestStareOutputDir)) {
fs.mkdirSync(jestStareOutputDir, {recursive: true});
}
// 'ios-results-*' is the path in CI where the parallel detox jobs save artifacts
await mergeJestStareJsonFiles(jestStareCombinedFilePath, [`${ARTIFACTS_DIR}/ios-results*/jest-stare/${platform}-data*.json`]);
generateJestStareHtmlReport(jestStareOutputDir, `${platform}-report.html`, jestStareCombinedFilePath);
if (process.env.CI) {
// Delete folders starting with "iOS-results-" only in CI environment
const entries = fs.readdirSync(ARTIFACTS_DIR, {withFileTypes: true});
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('ios-results-')) {
fs.rmSync(path.join(ARTIFACTS_DIR, entry.name), {recursive: true});
}
}
}
const result = await saveArtifacts();
if (result && result.success) {
console.log('Successfully uploaded artifacts to S3:', result.reportLink);

View file

@ -308,7 +308,7 @@ function generateTestReport(summary, isUploadedToS3, reportLink, environment, te
function generateTitle() {
const {
BRANCH,
BUILD_AWS_S3_BUCKET,
DETOX_AWS_S3_BUCKET,
BUILD_ID,
COMMIT_HASH,
IOS,
@ -332,7 +332,7 @@ function generateTitle() {
switch (TYPE) {
case 'PR':
buildLink = ` with [${lane}:${COMMIT_HASH}](https://${BUILD_AWS_S3_BUCKET}.s3.amazonaws.com/${s3Folder}/${appFilePath})`;
buildLink = ` with [${lane}:${COMMIT_HASH}](https://${DETOX_AWS_S3_BUCKET}.s3.amazonaws.com/${s3Folder}/${appFilePath})`;
title = `${platform} E2E for Pull Request Build: [${BRANCH}](${PULL_REQUEST})${buildLink}`;
break;
case 'RELEASE':