diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index 1d66c9f35338..acf097f89a1f 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: type: - description: 'What type of build to run. Must be one of ["release", "adhoc", "e2e", "e2eDelta"]' + description: 'What type of build to run. Must be one of ["release", "adhoc"]' type: string required: true ref: @@ -41,8 +41,6 @@ on: options: - release - adhoc - - e2e - - e2eDelta ref: description: Git ref to checkout and build required: true @@ -117,27 +115,11 @@ jobs: run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - name: Setup DotEnv - if: ${{ inputs.type != 'release' }} + if: ${{ inputs.type == 'adhoc' }} run: | - if [ '${{ inputs.type }}' == 'adhoc' ]; then - cp .env.staging .env.adhoc - sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc - echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc - else - envFile='' - if [ '${{ inputs.type }}' == 'e2e' ]; then - envFile='tests/e2e/.env.e2e' - else - envFile=tests/e2e/.env.e2edelta - fi - { - echo "EXPENSIFY_PARTNER_NAME=${{ secrets.EXPENSIFY_PARTNER_NAME }}" - echo "EXPENSIFY_PARTNER_PASSWORD=${{ secrets.EXPENSIFY_PARTNER_PASSWORD }}" - echo "EXPENSIFY_PARTNER_USER_ID=${{ secrets.EXPENSIFY_PARTNER_USER_ID }}" - echo "EXPENSIFY_PARTNER_USER_SECRET=${{ secrets.EXPENSIFY_PARTNER_USER_SECRET }}" - echo "EXPENSIFY_PARTNER_PASSWORD_EMAIL=${{ secrets.EXPENSIFY_PARTNER_PASSWORD_EMAIL }}" - } >> "$envFile" - fi + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=${{ inputs.pull_request_number }}" >> .env.adhoc - name: Build Android app (retryable) # v3 @@ -158,10 +140,6 @@ jobs: lane='build';; 'adhoc') lane='build_adhoc';; - 'e2e') - lane='build_e2e';; - 'e2eDelta') - lane='build_e2eDelta';; esac bundle exec fastlane android "$lane" diff --git a/.github/workflows/checkE2ETestCode.yml b/.github/workflows/checkE2ETestCode.yml deleted file mode 100644 index 89fc572427ea..000000000000 --- a/.github/workflows/checkE2ETestCode.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Check e2e test code builds correctly - -on: - workflow_call: - pull_request: - types: [opened, synchronize] - paths: - - 'tests/e2e/**' - - 'src/libs/E2E/**' - -jobs: - lint: - if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - name: Checkout - # v4 - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Verify e2e tests compile correctly - run: npm run e2e-test-runner-build - diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml deleted file mode 100644 index cd2c22f9b62d..000000000000 --- a/.github/workflows/e2ePerformanceTests.yml +++ /dev/null @@ -1,319 +0,0 @@ -name: E2E Performance Tests - -on: - workflow_call: - inputs: - PR_NUMBER: - description: A PR number to run performance tests against. If the PR is already merged, the merge commit will be used. If not, the PR will be merged locally before running the performance tests. - type: string - required: true - - workflow_dispatch: - inputs: - PR_NUMBER: - description: A PR number to run performance tests against. If the PR is already merged, the merge commit will be used. If not, the PR will be merged locally before running the performance tests. - type: string - required: true - -concurrency: - group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-e2e - cancel-in-progress: true - -jobs: - prep: - runs-on: blacksmith-2vcpu-ubuntu-2404 - name: Find the baseline and delta refs, and check for an existing build artifact for that commit - outputs: - BASELINE_REF: ${{ steps.getBaselineRef.outputs.BASELINE_REF }} - DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }} - IS_PR_MERGED: ${{ steps.getPullRequestDetails.outputs.IS_MERGED }} - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - with: - fetch-depth: 0 # Fetches the entire history - - - name: Determine "baseline ref" (prev merge commit) - id: getBaselineRef - run: | - # Get the name of the current branch - current_branch=$(git rev-parse --abbrev-ref HEAD) - - if [ "$current_branch" = "main" ]; then - # On the main branch, find the previous merge commit - previous_merge=$(git rev-list --merges HEAD~1 | head -n 1) - else - # On a feature branch, find the common ancestor of the current branch and main - git fetch origin main:main - previous_merge=$(git merge-base HEAD main) - fi - echo "$previous_merge" - echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT" - - - name: Get pull request details - id: getPullRequestDetails - uses: ./.github/actions/javascript/getPullRequestDetails - with: - GITHUB_TOKEN: ${{ github.token }} - PULL_REQUEST_NUMBER: ${{ inputs.PR_NUMBER }} - USER: ${{ github.actor }} - - - name: Determine "delta ref" - id: getDeltaRef - run: | - if [ '${{ steps.getPullRequestDetails.outputs.IS_MERGED }}' == 'true' ]; then - echo "DELTA_REF=${{ steps.getPullRequestDetails.outputs.MERGE_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" - else - # Set dummy git credentials - git config --global user.email "test@test.com" - git config --global user.name "Test" - - # Fetch head_ref of unmerged PR - git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - - # Merge pull request locally and get merge commit sha - git merge --allow-unrelated-histories -X ours --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - - # Create and push a branch so it can be checked out in another runner - git checkout -b e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - git push origin e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} - echo "DELTA_REF=e2eDelta-${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" - fi - - buildBaseline: - name: Build apk from baseline - uses: ./.github/workflows/buildAndroid.yml - needs: prep - secrets: inherit - with: - type: e2e - ref: ${{ needs.prep.outputs.BASELINE_REF }} - artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_REF }} - - buildDelta: - name: Build apk from delta ref - uses: ./.github/workflows/buildAndroid.yml - needs: prep - secrets: inherit - with: - type: e2eDelta - ref: ${{ needs.prep.outputs.DELTA_REF }} - artifact-prefix: delta-${{ needs.prep.outputs.DELTA_REF }} - - runTestsInAWS: - runs-on: blacksmith-2vcpu-ubuntu-2404 - needs: [prep, buildBaseline, buildDelta] - if: ${{ always() }} - name: Run E2E tests in AWS device farm - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - with: - # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify (we need a PAT to access the artifact API) - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Make zip directory for everything to send to AWS Device Farm - run: mkdir zip - - - name: Download baseline APK - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e - id: downloadBaselineAPK - with: - name: ${{ needs.buildBaseline.outputs.APK_ARTIFACT_NAME }} - path: zip - - # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - - name: Rename baseline APK - run: mv "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2e-release.apk" "${{ steps.downloadBaselineAPK.outputs.download-path }}/app-e2eRelease.apk" - - - name: Download delta APK - # v4 - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e - id: downloadDeltaAPK - with: - name: ${{ needs.buildDelta.outputs.APK_ARTIFACT_NAME }} - path: zip - - - name: Rename delta APK - run: mv "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edelta-release.apk" "${{ steps.downloadDeltaAPK.outputs.download-path }}/app-e2edeltaRelease.apk" - - - name: Compile test runner to be executable in a nodeJS environment - run: npm run e2e-test-runner-build - - - name: Copy e2e code into zip folder - run: cp tests/e2e/dist/index.js zip/testRunner.ts - - - name: Copy profiler binaries into zip folder - run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin - - - name: Zip everything in the zip directory up - run: zip -qr App.zip ./zip - - - name: Configure AWS Credentials - # v4 - uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-west-2 - - - name: Schedule AWS Device Farm test run on main branch - # v3.5.0 - uses: Wandalen/wretry.action@96605825122f94418f745bb54775d049581bbc6b - id: schedule-awsdf-main - with: - action: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b - with: | - name: App E2E Performance Regression Tests - project_arn: ${{ secrets.AWS_PROJECT_ARN }} - device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease.apk - app_type: ANDROID_APP - test_type: APPIUM_NODE - test_package_file: App.zip - test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpec.yml - test_spec_type: APPIUM_NODE_TEST_SPEC - remote_src: false - file_artifacts: | - Customer Artifacts.zip - Test spec output.txt - log_artifacts: debug.log - cleanup: true - timeout: 7200 - - - name: Print logs if run failed - if: failure() - run: | - echo ${{ steps.schedule-awsdf-main.outputs.data }} - unzip "Customer Artifacts.zip" -d mainResults - cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/logcat.txt" || true - cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log || true - cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt" || true - - - name: Announce failed workflow in Slack - if: failure() - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#e2e-announce', - attachments: [{ - color: 'danger', - text: `💥 ${process.env.AS_REPO} E2E Test run failed on workflow 💥`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - - name: Unzip AWS Device Farm results - if: always() - run: unzip "Customer Artifacts.zip" - - - name: Print AWS Device Farm run results - if: always() - run: | - if ls "./Host_Machine_Files/\$WORKING_DIRECTORY"/output2.md 1> /dev/null 2>&1; then - # Print all the split files - for file in "./Host_Machine_Files/\$WORKING_DIRECTORY/output"*; do - if [ -f "$file" ]; then - cat "$file" - fi - done - else - cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md" - fi - - - name: Check if test failed, if so post the results and add the DeployBlocker label - id: checkIfRegressionDetected - run: | - if grep -q '🔴' "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md"; then - # Create an output to the GH action that the test failed: - echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT" - - gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash - - # Check if there are any split files - if ls "./Host_Machine_Files/\$WORKING_DIRECTORY"/output2.md 1> /dev/null 2>&1; then - # Post each split file as a separate comment - for file in "./Host_Machine_Files/\$WORKING_DIRECTORY/output"*; do - if [ -f "$file" ]; then - gh pr comment ${{ inputs.PR_NUMBER }} -F "$file" - fi - done - else - gh pr comment ${{ inputs.PR_NUMBER }} -F "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md" - fi - - gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." - else - echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT" - echo '✅ no performance regression detected' - fi - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Check if test has skipped tests - id: checkIfSkippedTestsDetected - run: | - if grep -q '⚠️' "./Host_Machine_Files/\$WORKING_DIRECTORY/output1.md"; then - # Create an output to the GH action that the tests were skipped: - echo "skippedTestsDetected=true" >> "$GITHUB_OUTPUT" - else - echo "skippedTestsDetected=false" >> "$GITHUB_OUTPUT" - echo '✅ no skipped tests detected' - fi - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: 'Announce skipped tests in Slack' - if: ${{ steps.checkIfSkippedTestsDetected.outputs.skippedTestsDetected == 'true' }} - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#e2e-announce', - attachments: [{ - color: 'danger', - text: `⚠️ ${process.env.AS_REPO} Some of E2E tests were skipped on workflow ⚠️`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - - name: 'Announce regression in Slack' - if: ${{ steps.checkIfRegressionDetected.outputs.performanceRegressionDetected == 'true' }} - # v3 - uses: 8398a7/action-slack@1750b5085f3ec60384090fb7c52965ef822e869e - with: - status: custom - custom_payload: | - { - channel: '#quality', - attachments: [{ - color: 'danger', - text: `🔴 Performance regression detected in PR ${{ inputs.PR_NUMBER }}\nDetected in workflow.`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - - cleanupDeltaRef: - needs: [prep, runTestsInAWS] - if: ${{ always() && needs.prep.outputs.IS_PR_MERGED != 'true' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4 - - - name: Delete temporary merge branch created for delta ref - run: git push -d origin ${{ needs.prep.outputs.DELTA_REF }} diff --git a/.prettierignore b/.prettierignore index c367e104bcae..1545735a5895 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,9 +18,6 @@ package-lock.json *.markdown # We need to ignore index.js because it should always have plyfill on top of imports and we don't want prettier to sort them index.js -# We need to modify the import here specifically, hence we disable prettier to get rid of the sorted imports -src/libs/E2E/reactNativeLaunchingTest.ts - # Disable prettier in 3rd-party snippets web/snippets/** diff --git a/CLAUDE.md b/CLAUDE.md index 37aaf7907025..c06226055f7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,7 +154,6 @@ Key GitHub Actions workflows: - `test.yml`: Unit tests - `typecheck.yml`: TypeScript validation - `lint.yml`: Code quality checks -- `e2ePerformanceTests.yml`: Performance testing ## Related Repositories @@ -198,7 +197,6 @@ The skill provides guidance on: ### Testing - **Unit Tests**: Jest with React Native Testing Library -- **E2E Tests**: Custom test runner - **Performance Tests**: Reassure framework ## Special Considerations diff --git a/README.md b/README.md index 8588a973956c..d4edb9122dba 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,6 @@ variables referenced here get updated since your local `.env` file is ignored. see [PERFORMANCE.md](contributingGuides/PERFORMANCE.md#performance-metrics-opt-in-on-local-release-builds) for more information - `ONYX_METRICS` (optional) - Set this to `true` to capture even more performance metrics and see them in Flipper see [React-Native-Onyx#benchmarks](https://github.com/Expensify/react-native-onyx#benchmarks) for more information -- `E2E_TESTING` (optional) - This needs to be set to `true` when running the e2e tests for performance regression testing. - This happens usually automatically, read [this](tests/e2e/README.md) for more information > If your changes to .env aren't having an effect, try `rm -rf .rock`, then re-run `npm run ios` or `npm run android` diff --git a/android/app/build.gradle b/android/app/build.gradle index 6aa5acf23c4b..0b7d799451f9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -75,8 +75,6 @@ project.ext.envConfigFiles = [ adhocRelease: ".env.adhoc", developmentRelease: ".env", developmentDebug: ".env", - e2eRelease: "tests/e2e/.env.e2e", - e2edeltaRelease: "tests/e2e/.env.e2edelta" ] /** @@ -124,20 +122,6 @@ android { productFlavors { // we need to define a production flavor but since it has default config, we can leave it empty production - e2e { - // If are building a version that won't be uploaded to the play store, we don't have to use production keys - // applies all non-production flavors - applicationIdSuffix ".e2e" - signingConfig signingConfigs.debug - resValue "string", "build_config_package", "com.expensify.chat" - } - e2edelta { - // If are building a version that won't be uploaded to the play store, we don't have to use production keys - // applies all non-production flavors - applicationIdSuffix ".e2edelta" - signingConfig signingConfigs.debug - resValue "string", "build_config_package", "com.expensify.chat" - } adhoc { applicationIdSuffix ".adhoc" signingConfig signingConfigs.debug @@ -174,23 +158,18 @@ android { minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" - signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, - // thus we need to manually set the signing config, so that the e2e uses the debug config again. - // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. + // so we need to manually set the signing config per flavor in release mode. + signingConfig null productFlavors.all { flavor -> - // All release builds should be signed with the release config ... flavor.signingConfig signingConfigs.release } - // ... except for the e2e flavor, which we maybe want to build locally: - productFlavors.e2e.signingConfig signingConfigs.debug - productFlavors.e2edelta.signingConfig signingConfigs.debug } } - // since we don't need variants adhocDebug and e2eDebug, we can force gradle to ignore them + // Filter out unnecessary build variants variantFilter { variant -> - if (variant.name == "adhocDebug" || variant.name == "e2eDebug" || variant.name == "e2edeltaDebug") { + if (variant.name == "adhocDebug") { setIgnore(true) } } diff --git a/android/app/src/e2e/AndroidManifest.xml b/android/app/src/e2e/AndroidManifest.xml deleted file mode 100644 index 201d730f5211..000000000000 --- a/android/app/src/e2e/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/android/app/src/e2edelta/AndroidManifest.xml b/android/app/src/e2edelta/AndroidManifest.xml deleted file mode 100644 index 201d730f5211..000000000000 --- a/android/app/src/e2edelta/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/babel.config.js b/babel.config.js index f262c5454a7c..66094c956fac 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,5 @@ require('dotenv').config(); -const IS_E2E_TESTING = process.env.E2E_TESTING === 'true'; - const ReactCompilerConfig = { target: '19', environment: { @@ -131,8 +129,7 @@ const metro = { ], env: { production: { - // Keep console logs for e2e tests - plugins: IS_E2E_TESTING ? [] : [['transform-remove-console', {exclude: ['error', 'warn']}]], + plugins: [['transform-remove-console', {exclude: ['error', 'warn']}]], }, }, }; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7dfcec4c7c30..50617f9db9e3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -126,35 +126,6 @@ platform :android do setGradleOutputsInEnv() end - desc "Generate a new local APK for e2e testing" - lane :build_e2e do - ENV["ENVFILE"]="tests/e2e/.env.e2e" - ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.ts" - ENV["E2E_TESTING"]="true" - - gradle( - project_dir: './android', - task: ':app:assemble', - flavor: 'e2e', - build_type: 'Release', - ) - setGradleOutputsInEnv() - end - - lane :build_e2eDelta do - ENV["ENVFILE"]="tests/e2e/.env.e2edelta" - ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.ts" - ENV["E2E_TESTING"]="true" - - gradle( - project_dir: './android', - task: ':app:assemble', - flavor: 'e2edelta', - build_type: 'Release', - ) - setGradleOutputsInEnv() - end - desc "Build AdHoc testing build" lane :build_adhoc do ENV["ENVFILE"]=".env.adhoc" diff --git a/metro.config.js b/metro.config.js index 59479c07b374..3ea23f7f2067 100644 --- a/metro.config.js +++ b/metro.config.js @@ -15,9 +15,6 @@ require('dotenv').config({path: envPath}); const defaultConfig = getReactNativeDefaultConfig(__dirname); const expoConfig = getExpoDefaultConfig(__dirname); -const isE2ETesting = process.env.E2E_TESTING === 'true'; -const e2eSourceExts = ['e2e.js', 'e2e.ts', 'e2e.tsx']; - const isDev = process.env.ENVIRONMENT === undefined || process.env.ENVIRONMENT === 'development'; /** @@ -29,8 +26,7 @@ const isDev = process.env.ENVIRONMENT === undefined || process.env.ENVIRONMENT = const config = { resolver: { assetExts: [...defaultConfig.resolver.assetExts, 'lottie'], - // When we run the e2e tests we want files that have the extension e2e.js to be resolved as source files - sourceExts: [...(isE2ETesting ? e2eSourceExts : []), ...defaultConfig.resolver.sourceExts, ...defaultConfig.watcher.additionalExts, 'jsx'], + sourceExts: [...defaultConfig.resolver.sourceExts, ...defaultConfig.watcher.additionalExts, 'jsx'], }, // We are merging the default config from Expo and React Native and expo one is overriding the React Native one so inlineRequires is set to false so we want to set it to true // for fix cycling dependencies and improve performance of app startup diff --git a/package-lock.json b/package-lock.json index 07d1db9aea69..3ff9311eef24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -173,9 +173,6 @@ "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", "@octokit/webhooks-types": "^7.5.1", - "@perf-profiler/profiler": "^0.10.10", - "@perf-profiler/reporter": "^0.9.0", - "@perf-profiler/types": "^0.8.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@prettier/plugin-oxc": "^0.1.3", "@react-native-community/cli": "20.0.0", @@ -10926,88 +10923,6 @@ "node": ">=10" } }, - "node_modules/@perf-profiler/android": { - "version": "0.13.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@perf-profiler/logger": "^0.3.3", - "@perf-profiler/profiler": "^0.10.11", - "@perf-profiler/types": "^0.8.0", - "commander": "^12.0.0", - "lodash": "^4.17.21" - }, - "bin": { - "perf-profiler-commands": "dist/src/commands.js" - } - }, - "node_modules/@perf-profiler/ios": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@perf-profiler/ios-instruments": "^0.3.3", - "@perf-profiler/logger": "^0.3.3", - "@perf-profiler/types": "^0.8.0" - } - }, - "node_modules/@perf-profiler/ios-instruments": { - "version": "0.3.3", - "dev": true, - "license": "ISC", - "dependencies": { - "@perf-profiler/logger": "^0.3.3", - "@perf-profiler/profiler": "^0.10.11", - "@perf-profiler/types": "^0.8.0", - "commander": "^12.0.0", - "fast-xml-parser": "^4.2.7" - }, - "bin": { - "flashlight-ios-poc": "dist/launchIOS.js" - } - }, - "node_modules/@perf-profiler/logger": { - "version": "0.3.3", - "dev": true, - "dependencies": { - "kleur": "^4.1.5", - "luxon": "^3.4.4" - }, - "bin": { - "perf-profiler-logger": "dist/bin.js" - } - }, - "node_modules/@perf-profiler/logger/node_modules/kleur": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@perf-profiler/profiler": { - "version": "0.10.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@perf-profiler/android": "^0.13.0", - "@perf-profiler/ios": "^0.3.3", - "@perf-profiler/types": "^0.8.0" - } - }, - "node_modules/@perf-profiler/reporter": { - "version": "0.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@perf-profiler/types": "^0.8.0", - "lodash": "^4.17.21" - } - }, - "node_modules/@perf-profiler/types": { - "version": "0.8.0", - "dev": true - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -30593,14 +30508,6 @@ "node": ">=10" } }, - "node_modules/luxon": { - "version": "3.4.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index aa8f603aa47a..462fd4631082 100644 --- a/package.json +++ b/package.json @@ -63,11 +63,8 @@ "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "combine-web-sourcemaps": "./scripts/combine-web-sourcemaps.ts", - "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", - "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "npx ts-node scripts/findUnusedStyles.ts", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", - "e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", "react-compiler-compliance-check": "ts-node scripts/react-compiler-compliance-check.ts", "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy", "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy && ./scripts/parser-workletization.sh src/libs/SearchParser/autocompleteParser.js", @@ -239,9 +236,6 @@ "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", "@octokit/webhooks-types": "^7.5.1", - "@perf-profiler/profiler": "^0.10.10", - "@perf-profiler/reporter": "^0.9.0", - "@perf-profiler/types": "^0.8.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@prettier/plugin-oxc": "^0.1.3", "@react-native-community/cli": "20.0.0", diff --git a/patches/perf-profiler/@perf-profiler+android+0.13.0+001+pid-changed.patch b/patches/perf-profiler/@perf-profiler+android+0.13.0+001+pid-changed.patch deleted file mode 100644 index d607b1460d79..000000000000 --- a/patches/perf-profiler/@perf-profiler+android+0.13.0+001+pid-changed.patch +++ /dev/null @@ -1,45 +0,0 @@ -diff --git a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -index 657c3b0..c97e363 100644 ---- a/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -+++ b/node_modules/@perf-profiler/android/dist/src/commands/platforms/UnixProfiler.js -@@ -113,7 +113,7 @@ class UnixProfiler { - } - pollPerformanceMeasures(bundleId, { onMeasure, onStartMeasuring = () => { - // noop by default -- }, }) { -+ }, onPidChanged = () => {}}) { - let initialTime = null; - let previousTime = null; - let cpuMeasuresAggregator = new CpuMeasureAggregator_1.CpuMeasureAggregator(this.getCpuClockTick()); -@@ -170,6 +170,7 @@ class UnixProfiler { - previousTime = timestamp; - }, () => { - logger_1.Logger.warn("Process id has changed, ignoring measures until now"); -+ onPidChanged(); - reset(); - }); - } -diff --git a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -index be26fe6..0473f78 100644 ---- a/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -+++ b/node_modules/@perf-profiler/android/src/commands/platforms/UnixProfiler.ts -@@ -105,9 +105,11 @@ export abstract class UnixProfiler implements Profiler { - onStartMeasuring = () => { - // noop by default - }, -+ onPidChanged = () => {}, - }: { - onMeasure: (measure: Measure) => void; - onStartMeasuring?: () => void; -+ onPidChanged?: () => void; - } - ) { - let initialTime: number | null = null; -@@ -187,6 +189,7 @@ export abstract class UnixProfiler implements Profiler { - }, - () => { - Logger.warn("Process id has changed, ignoring measures until now"); -+ onPidChanged(); - reset(); - } - ); diff --git a/patches/perf-profiler/@perf-profiler+types+0.8.0+001+pid-changed.patch b/patches/perf-profiler/@perf-profiler+types+0.8.0+001+pid-changed.patch deleted file mode 100644 index bb24b9e880cf..000000000000 --- a/patches/perf-profiler/@perf-profiler+types+0.8.0+001+pid-changed.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/@perf-profiler/types/dist/index.d.ts b/node_modules/@perf-profiler/types/dist/index.d.ts -index 0d0f55f..ef7f864 100644 ---- a/node_modules/@perf-profiler/types/dist/index.d.ts -+++ b/node_modules/@perf-profiler/types/dist/index.d.ts -@@ -80,6 +80,7 @@ export interface ScreenRecorder { - export interface ProfilerPollingOptions { - onMeasure: (measure: Measure) => void; - onStartMeasuring?: () => void; -+ onPidChanged?: () => void; - } - export interface Profiler { - pollPerformanceMeasures: (bundleId: string, options: ProfilerPollingOptions) => { diff --git a/patches/perf-profiler/details.md b/patches/perf-profiler/details.md deleted file mode 100644 index 0d0d7c70f9e5..000000000000 --- a/patches/perf-profiler/details.md +++ /dev/null @@ -1,25 +0,0 @@ -# `perf-profiler` patches - -### [@perf-profiler+android+0.13.0+001+pid-changed.patch](@perf-profiler+android+0.13.0+001+pid-changed.patch) - -- Reason: - - ``` - This patch handles process id has changed. - ``` - -- Upstream PR/issue: 🛑, commented in the App PR https://github.com/Expensify/App/pull/46279#issuecomment-3346301071 -- E/App issue: https://github.com/Expensify/App/issues/46129 -- PR introducing patch: https://github.com/Expensify/App/pull/46279 - -### [@perf-profiler+types+0.8.0+001+pid-changed.patch](@perf-profiler+types+0.8.0+001+pid-changed.patch) - -- Reason: - - ``` - This patch handles process id has changed. - ``` - -- Upstream PR/issue: 🛑, commented in the App PR https://github.com/Expensify/App/pull/46279#issuecomment-3346301071 -- E/App issue: https://github.com/Expensify/App/issues/46129 -- PR introducing patch: https://github.com/Expensify/App/pull/46279 diff --git a/src/CONFIG.ts b/src/CONFIG.ts index e7cc4d2f7ef0..50f07607fd43 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -106,7 +106,6 @@ export default { CAPTURE_METRICS: get(Config, 'CAPTURE_METRICS', 'false') === 'true', ONYX_METRICS: get(Config, 'ONYX_METRICS', 'false') === 'true', DEV_PORT: process.env.PORT ?? 8082, - E2E_TESTING: get(Config, 'E2E_TESTING', 'false') === 'true', SEND_CRASH_REPORTS: get(Config, 'SEND_CRASH_REPORTS', 'false') === 'true', IS_USING_WEB_PROXY: getPlatform() === 'web' && useWebProxy, APPLE_SIGN_IN: { diff --git a/src/components/Composer/index.e2e.tsx b/src/components/Composer/index.e2e.tsx deleted file mode 100644 index 345c2bec94e8..000000000000 --- a/src/components/Composer/index.e2e.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import Composer from './implementation'; -import type {ComposerProps} from './types'; - -function ComposerE2E(props: ComposerProps) { - return ( - - ); -} - -export default ComposerE2E; diff --git a/src/components/InvertedFlatList/index.e2e.tsx b/src/components/InvertedFlatList/index.e2e.tsx deleted file mode 100644 index caa08ea9b023..000000000000 --- a/src/components/InvertedFlatList/index.e2e.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import type {ScrollViewProps, ViewToken} from 'react-native'; -import {DeviceEventEmitter, FlatList} from 'react-native'; -import type {ReportAction} from '@src/types/onyx'; -import type {InvertedFlatListProps} from './types'; - -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; - -function InvertedFlatListE2E({ref, ...props}: InvertedFlatListProps) { - const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; - - const handleViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => { - DeviceEventEmitter.emit('onViewableItemsChanged', viewableItems); - }; - - const config: ScrollViewProps['maintainVisibleContentPosition'] = { - // This needs to be 1 to avoid using loading views as anchors. - minIndexForVisible: rest.data?.length ? Math.min(1, rest.data.length - 1) : 0, - }; - - if (shouldEnableAutoScrollToTopThreshold) { - config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; - } - - return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...rest} - ref={ref} - maintainVisibleContentPosition={config} - inverted - onViewableItemsChanged={handleViewableItemsChanged} - /> - ); -} - -export default InvertedFlatListE2E; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 12a9dd10d76d..87a5e93c3750 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -69,7 +69,7 @@ type PopoverMenuItem = MenuItemProps & { /** Whether to keep the modal open after clicking on the menu item */ shouldKeepModalOpen?: boolean; - /** Test identifier used to find elements in unit and e2e tests */ + /** Test identifier used to find elements in tests */ testID?: string; /** Whether to show a loading spinner icon for the menu item */ diff --git a/src/components/Pressable/GenericPressable/index.e2e.tsx b/src/components/Pressable/GenericPressable/index.e2e.tsx deleted file mode 100644 index 5556765bd94f..000000000000 --- a/src/components/Pressable/GenericPressable/index.e2e.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, {useEffect} from 'react'; -import {DeviceEventEmitter} from 'react-native'; -import GenericPressable from './implementation'; -import type PressableProps from './types'; - -const pressableRegistry = new Map(); - -function getPressableProps(testId: string): PressableProps | undefined { - return pressableRegistry.get(testId); -} - -function E2EGenericPressableWrapper({ref, ...props}: PressableProps) { - useEffect(() => { - const testId = props.testID; - if (!testId) { - return; - } - console.debug(`[E2E] E2EGenericPressableWrapper: Registering pressable with testID: ${testId}`); - pressableRegistry.set(testId, props); - - DeviceEventEmitter.emit('onBecameVisible', testId); - }, [props]); - - return ( - - ); -} - -export default E2EGenericPressableWrapper; -export {getPressableProps}; diff --git a/src/components/TabSelector/types.ts b/src/components/TabSelector/types.ts index 632ac790e50a..aaf68dcb629b 100644 --- a/src/components/TabSelector/types.ts +++ b/src/components/TabSelector/types.ts @@ -90,7 +90,7 @@ type TabSelectorItemProps = WithSentryLabel & { /** Whether to show the label when the tab is inactive */ shouldShowLabelWhenInactive?: boolean; - /** Test identifier used to find elements in unit and e2e tests */ + /** Test identifier used to find elements in tests */ testID?: string; /** Whether tabs should have equal width */ diff --git a/src/components/TextInput/BaseTextInput/index.e2e.tsx b/src/components/TextInput/BaseTextInput/index.e2e.tsx deleted file mode 100644 index f38629a192ac..000000000000 --- a/src/components/TextInput/BaseTextInput/index.e2e.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, {useEffect} from 'react'; -import {DeviceEventEmitter} from 'react-native'; -import BaseTextInput from './implementation'; -import type {BaseTextInputProps} from './types'; - -function BaseTextInputE2E({ref, ...props}: BaseTextInputProps) { - useEffect(() => { - const testId = props.testID; - if (!testId) { - return; - } - console.debug(`[E2E] BaseTextInput: text-input with testID: ${testId} changed text to ${props.value}`); - - DeviceEventEmitter.emit('onChangeText', {testID: testId, value: props.value}); - }, [props.value, props.testID]); - - return ( - - ); -} - -export default BaseTextInputE2E; diff --git a/src/libs/E2E/actions/e2eLogin.ts b/src/libs/E2E/actions/e2eLogin.ts deleted file mode 100644 index a1a266fd156d..000000000000 --- a/src/libs/E2E/actions/e2eLogin.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable rulesdir/prefer-actions-set-data */ -/* eslint-disable rulesdir/prefer-onyx-connect-in-libs */ -import Onyx from 'react-native-onyx'; -import {Authenticate} from '@libs/Authentication'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import CONFIG from '@src/CONFIG'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const e2eUserCredentials = { - email: getConfigValueOrThrow('EXPENSIFY_PARTNER_PASSWORD_EMAIL'), - partnerUserID: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_ID'), - partnerUserSecret: getConfigValueOrThrow('EXPENSIFY_PARTNER_USER_SECRET'), - partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, - partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, -}; - -/** - * Command for e2e test to automatically sign in a user. - * If the user is already logged in the function will simply - * resolve. - * - * @return Resolved true when the user was actually signed in. Returns false if the user was already logged in. - */ -export default function (): Promise { - const waitForBeginSignInToFinish = (): Promise => - new Promise((resolve) => { - // We opted for `connectWithoutView` here as this is being used for mocking data for E2E flow. - const id = Onyx.connectWithoutView({ - key: ONYXKEYS.CREDENTIALS, - callback: (credentials) => { - // beginSignUp writes to credentials.login once the API call is complete - if (!credentials?.login) { - return; - } - - resolve(); - Onyx.disconnect(id); - }, - }); - }); - - let neededLogin = false; - - // Subscribe to auth token, to check if we are authenticated - return new Promise((resolve, reject) => { - // We opted for `connectWithoutView` here as this is being used for mocking data for E2E flow. - const connection = Onyx.connectWithoutView({ - key: ONYXKEYS.SESSION, - callback: (session) => { - if (session?.authToken == null || session.authToken.length === 0) { - neededLogin = true; - - // authenticate with a predefined user - console.debug('[E2E] Signing in…'); - Authenticate(e2eUserCredentials) - .then((response) => { - if (!response) { - return; - } - Onyx.merge(ONYXKEYS.SESSION, { - authToken: response.authToken, - creationDate: new Date().getTime(), - email: e2eUserCredentials.email, - }); - console.debug('[E2E] Signed in finished!'); - return waitForBeginSignInToFinish(); - }) - .catch((error) => { - console.error('[E2E] Error while signing in', error); - reject(error); - }); - } - // signal that auth was completed - resolve(neededLogin); - Onyx.disconnect(connection); - }, - }); - }); -} diff --git a/src/libs/E2E/actions/waitForAppLoaded.ts b/src/libs/E2E/actions/waitForAppLoaded.ts deleted file mode 100644 index 7aaf5f5a46ea..000000000000 --- a/src/libs/E2E/actions/waitForAppLoaded.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; - -// Once we get the sidebar loaded end mark we know that the app is ready to be used: -export default function waitForAppLoaded(): Promise { - return new Promise((resolve) => { - // We have used `connectWithoutView` here because it is not connected to any UI - const connection = Onyx.connectWithoutView({ - key: ONYXKEYS.IS_SIDEBAR_LOADED, - callback: (isSidebarLoaded) => { - if (!isSidebarLoaded) { - return; - } - - resolve(); - Onyx.disconnect(connection); - }, - }); - }); -} diff --git a/src/libs/E2E/actions/waitForKeyboard.ts b/src/libs/E2E/actions/waitForKeyboard.ts deleted file mode 100644 index df7a9aae9651..000000000000 --- a/src/libs/E2E/actions/waitForKeyboard.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Keyboard} from 'react-native'; - -export default function waitForKeyboard(): Promise { - return new Promise((resolve) => { - function checkKeyboard() { - if (Keyboard.isVisible()) { - resolve(); - } else { - console.debug(`[E2E] Waiting for keyboard to appear…`); - setTimeout(checkKeyboard, 1000); - } - } - checkKeyboard(); - }); -} diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts deleted file mode 100644 index b9965b7da0cb..000000000000 --- a/src/libs/E2E/client.ts +++ /dev/null @@ -1,110 +0,0 @@ -import Config from '../../../tests/e2e/config'; -import Routes from '../../../tests/e2e/server/routes'; -import type {NetworkCacheMap, TestConfig, TestResult} from './types'; -import {waitForActiveRequestsToBeEmpty} from './utils/NetworkInterceptor'; - -type NativeCommandPayload = { - text: string; -}; - -type NativeCommand = { - actionName: string; - payload?: NativeCommandPayload; -}; - -const SERVER_ADDRESS = `http://localhost:${Config.SERVER_PORT}`; - -const defaultHeaders = { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-E2E-Server-Request': 'true', -}; - -const defaultRequestInit: RequestInit = { - headers: defaultHeaders, -}; - -const sendRequest = (url: string, data: Record): Promise => { - return fetch(url, { - method: 'POST', - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'Content-Type': 'application/json', - ...defaultHeaders, - }, - body: JSON.stringify(data), - }).then((res) => { - if (res.status === 200) { - return res; - } - const errorMsg = `[E2E] Client failed to send request to "${url}". Returned status: ${res.status}`; - return res - .json() - .then((responseText) => { - throw new Error(`${errorMsg}: ${responseText}`); - }) - .catch(() => { - throw new Error(errorMsg); - }); - }); -}; - -/** - * Submits a test result to the server. - * Note: a test can have multiple test results. - */ -const submitTestResults = (testResult: TestResult): Promise => { - console.debug(`[E2E] Submitting test result '${testResult.name}'…`); - return sendRequest(`${SERVER_ADDRESS}${Routes.testResults}`, testResult).then(() => { - console.debug(`[E2E] Test result '${testResult.name}' submitted successfully`); - }); -}; - -const submitTestDone = () => waitForActiveRequestsToBeEmpty().then(() => fetch(`${SERVER_ADDRESS}${Routes.testDone}`, defaultRequestInit)); - -let currentActiveTestConfig: TestConfig | null = null; - -const getTestConfig = (): Promise => - fetch(`${SERVER_ADDRESS}${Routes.testConfig}`, defaultRequestInit) - .then((res: Response): Promise => res.json()) - .then((config: TestConfig) => { - currentActiveTestConfig = config; - return config; - }); - -const getCurrentActiveTestConfig = () => currentActiveTestConfig; - -const sendNativeCommand = (payload: NativeCommand) => { - console.debug(`[E2E] Sending native command '${payload.actionName}'…`); - return sendRequest(`${SERVER_ADDRESS}${Routes.testNativeCommand}`, payload).then(() => { - console.debug(`[E2E] Native command '${payload.actionName}' sent successfully`); - }); -}; - -const updateNetworkCache = (appInstanceId: string, networkCache: NetworkCacheMap) => { - console.debug('[E2E] Updating network cache…'); - return sendRequest(`${SERVER_ADDRESS}${Routes.testUpdateNetworkCache}`, { - appInstanceId, - cache: networkCache, - }).then(() => { - console.debug('[E2E] Network cache updated successfully'); - }); -}; - -const getNetworkCache = (appInstanceId: string): Promise => - sendRequest(`${SERVER_ADDRESS}${Routes.testGetNetworkCache}`, {appInstanceId}) - .then((res): Promise => res.json()) - .then((networkCache: NetworkCacheMap) => { - console.debug('[E2E] Network cache fetched successfully'); - return networkCache; - }); - -export default { - submitTestResults, - submitTestDone, - getTestConfig, - getCurrentActiveTestConfig, - sendNativeCommand, - updateNetworkCache, - getNetworkCache, -}; -export type {TestResult, NativeCommand, NativeCommandPayload}; diff --git a/src/libs/E2E/interactions/index.ts b/src/libs/E2E/interactions/index.ts deleted file mode 100644 index d4ce9511d8bb..000000000000 --- a/src/libs/E2E/interactions/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type {GestureResponderEvent} from 'react-native'; -import {DeviceEventEmitter} from 'react-native'; -import * as E2EGenericPressableWrapper from '@components/Pressable/GenericPressable/index.e2e'; -import Performance from '@libs/Performance'; - -const waitForElement = (testID: string) => { - console.debug(`[E2E] waitForElement: ${testID}`); - - if (E2EGenericPressableWrapper.getPressableProps(testID)) { - return Promise.resolve(); - } - - return new Promise((resolve) => { - const subscription = DeviceEventEmitter.addListener('onBecameVisible', (_testID: string) => { - if (_testID !== testID) { - return; - } - - subscription.remove(); - resolve(undefined); - }); - }); -}; - -const waitForTextInputValue = (text: string, _testID: string): Promise => { - return new Promise((resolve) => { - const subscription = DeviceEventEmitter.addListener('onChangeText', ({testID, value}) => { - if (_testID !== testID || value !== text) { - return; - } - - subscription.remove(); - resolve(undefined); - }); - }); -}; - -const waitForEvent = (eventName: string): Promise => { - return new Promise((resolve) => { - Performance.subscribeToMeasurements((entry) => { - if (entry.name !== eventName) { - return; - } - - resolve(entry); - }); - }); -}; - -const tap = (testID: string) => { - console.debug(`[E2E] Press on: ${testID}`); - - E2EGenericPressableWrapper.getPressableProps(testID)?.onPress?.({} as unknown as GestureResponderEvent); -}; - -export {waitForElement, tap, waitForEvent, waitForTextInputValue}; diff --git a/src/libs/E2E/isE2ETestSession.native.ts b/src/libs/E2E/isE2ETestSession.native.ts deleted file mode 100644 index 249062289e43..000000000000 --- a/src/libs/E2E/isE2ETestSession.native.ts +++ /dev/null @@ -1,6 +0,0 @@ -import CONFIG from '@src/CONFIG'; -import type {IsE2ETestSession} from './types'; - -const isE2ETestSession: IsE2ETestSession = () => CONFIG.E2E_TESTING; - -export default isE2ETestSession; diff --git a/src/libs/E2E/isE2ETestSession.ts b/src/libs/E2E/isE2ETestSession.ts deleted file mode 100644 index cba31689f951..000000000000 --- a/src/libs/E2E/isE2ETestSession.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {IsE2ETestSession} from './types'; - -const isE2ETestSession: IsE2ETestSession = () => false; - -export default isE2ETestSession; diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts deleted file mode 100644 index 507067ff4c31..000000000000 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable import/newline-after-import,import/first */ - -/** - * We are using a separate entry point for the E2E tests. - * By doing this, we avoid bundling any E2E testing code - * into the actual release app. - */ -import canCapturePerformanceMetrics from '@libs/Metrics'; -import Performance from '@libs/Performance'; -import Config from 'react-native-config'; -import E2EConfig from '../../../tests/e2e/config'; -import E2EClient from './client'; -import installNetworkInterceptor from './utils/NetworkInterceptor'; -import LaunchArgs from './utils/LaunchArgs'; -import type {TestModule, Tests} from './types'; - -console.debug('=========================='); -console.debug('==== Running e2e test ===='); -console.debug('=========================='); - -// Check if the performance module is available -if (!canCapturePerformanceMetrics()) { - throw new Error('Performance module not available! Please set CAPTURE_METRICS=true in your environment file!'); -} - -const appInstanceId = Config.E2E_BRANCH -if (!appInstanceId) { - throw new Error('E2E_BRANCH not set in environment file!'); -} - - -// import your test here, define its name and config first in e2e/config.js -const tests: Tests = { - [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, - [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, - [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, - [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, - [E2EConfig.TEST_NAMES.MoneyRequest]: require('./tests/moneyRequestTest.e2e').default, -}; - -// Once we receive the TII measurement we know that the app is initialized and ready to be used: -const appReady = new Promise((resolve) => { - Performance.subscribeToMeasurements((entry) => { - if (entry.name !== 'TTI') { - return; - } - - resolve(); - }); -}); - -// Install the network interceptor -installNetworkInterceptor( - () => E2EClient.getNetworkCache(appInstanceId), - (networkCache) => E2EClient.updateNetworkCache(appInstanceId, networkCache), - LaunchArgs.mockNetwork ?? false -) - -E2EClient.getTestConfig() - .then((config): Promise | undefined => { - const test = tests[config.name]; - if (!test) { - console.error(`[E2E] Test '${config.name}' not found`); - // instead of throwing, report the error to the server, which is better for DX - return E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: config.name, - error: `Test '${config.name}' not found`, - isCritical: false, - }); - } - - console.debug(`[E2E] Configured for test ${config.name}. Waiting for app to become ready`); - appReady - .then(() => { - console.debug('[E2E] App is ready, running test…'); - Performance.measureFailSafe('appStartedToReady', 'regularAppStart'); - test(config); - }) - .catch((error) => { - console.error('[E2E] Error while waiting for app to become ready', error); - }); - }) - .catch((error) => { - console.error("[E2E] Error while running test. Couldn't get test config!", error); - }); - -// start the usual app -Performance.markStart('regularAppStart'); -import '../../../index'; -Performance.markEnd('regularAppStart'); diff --git a/src/libs/E2E/tests/appStartTimeTest.e2e.ts b/src/libs/E2E/tests/appStartTimeTest.e2e.ts deleted file mode 100644 index ccd781e08514..000000000000 --- a/src/libs/E2E/tests/appStartTimeTest.e2e.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Config from 'react-native-config'; -import type {NativeConfig} from 'react-native-config'; -import type {PerformanceEntry} from 'react-native-performance'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import E2EClient from '@libs/E2E/client'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import Performance from '@libs/Performance'; - -const test = (config: NativeConfig) => { - const name = getConfigValueOrThrow('name', config); - // check for login (if already logged in the action will simply resolve) - E2ELogin().then((neededLogin) => { - if (neededLogin) { - return waitForAppLoaded().then(() => - // we don't want to submit the first login to the results - E2EClient.submitTestDone(), - ); - } - - console.debug('[E2E] Logged in, getting metrics and submitting them…'); - - // collect performance metrics and submit - const metrics: PerformanceEntry[] = Performance.getPerformanceMetrics(); - - // promises in sequence without for-loop - Promise.all( - metrics.map((metric) => - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} ${metric.name}`, - metric: metric.duration, - unit: 'ms', - }), - ), - ) - .then(() => { - console.debug('[E2E] Done, exiting…'); - E2EClient.submitTestDone(); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts deleted file mode 100644 index 62a01e43755d..000000000000 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ /dev/null @@ -1,67 +0,0 @@ -import Config from 'react-native-config'; -import type {NativeConfig} from 'react-native-config'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import E2EClient from '@libs/E2E/client'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; - -const test = (config: NativeConfig) => { - // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for chat opening'); - - const reportID = getConfigValueOrThrow('reportID', config); - const name = getConfigValueOrThrow('name', config); - - E2ELogin().then((neededLogin) => { - if (neededLogin) { - return waitForAppLoaded().then(() => - // we don't want to submit the first login to the results - E2EClient.submitTestDone(), - ); - } - - console.debug('[E2E] Logged in, getting chat opening metrics and submitting them…'); - - const [chatTTIPromise, chatTTIResolve] = getPromiseWithResolve(); - - chatTTIPromise.then(() => { - console.debug(`[E2E] Submitting!`); - - E2EClient.submitTestDone(); - }); - - Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to report…`); - Performance.markStart(CONST.TIMING.OPEN_REPORT); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - return; - } - - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - - if (entry.name === CONST.TIMING.OPEN_REPORT) { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} Chat TTI`, - metric: entry.duration, - unit: 'ms', - }) - .then(() => { - console.debug('[E2E] Done with chat TTI tracking, exiting…'); - chatTTIResolve(); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - } - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/tests/linkingTest.e2e.ts b/src/libs/E2E/tests/linkingTest.e2e.ts deleted file mode 100644 index 38c659c0efe3..000000000000 --- a/src/libs/E2E/tests/linkingTest.e2e.ts +++ /dev/null @@ -1,116 +0,0 @@ -import {DeviceEventEmitter} from 'react-native'; -import Config from 'react-native-config'; -import type {NativeConfig} from 'react-native-config'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import E2EClient from '@libs/E2E/client'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; - -type ViewableItem = { - reportActionID?: string; -}; - -type ViewableItemResponse = Array<{item?: ViewableItem}>; - -const test = (config: NativeConfig) => { - console.debug('[E2E] Logging in for comment linking'); - - const reportID = getConfigValueOrThrow('reportID', config); - const linkedReportID = getConfigValueOrThrow('linkedReportID', config); - const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config); - const name = getConfigValueOrThrow('name', config); - - const startTestTime = Date.now(); - console.debug('[E2E] Test started at:', startTestTime); - - E2ELogin().then((neededLogin) => { - if (neededLogin) { - return waitForAppLoaded().then(() => E2EClient.submitTestDone()); - } - - const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve(); - const [openReportPromise, openReportResolve] = getPromiseWithResolve(); - let lastVisibleMessageId: string | undefined; - let verificationStarted = false; - let hasNavigatedToLinkedMessage = false; - - const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res: ViewableItemResponse) => { - console.debug('[E2E] Viewable items event triggered at:', Date.now()); - - // update the last visible message - lastVisibleMessageId = res?.at(0)?.item?.reportActionID; - console.debug('[E2E] Current visible message:', lastVisibleMessageId); - - if (!verificationStarted && lastVisibleMessageId === linkedReportActionID) { - console.debug('[E2E] Target message found, starting verification'); - verificationStarted = true; - - setTimeout(() => { - console.debug('[E2E] Verification timeout completed'); - console.debug('[E2E] Last visible message ID:', lastVisibleMessageId); - console.debug('[E2E] Expected message ID:', linkedReportActionID); - - subscription.remove(); - if (lastVisibleMessageId === linkedReportActionID) { - console.debug('[E2E] Message position verified successfully'); - appearMessageResolve(); - } else { - console.debug('[E2E] Linked message not found, failing test!'); - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - error: 'Linked message not found', - name: `${name} test can't find linked message`, - }).then(() => E2EClient.submitTestDone()); - } - }, 3000); - } - }); - - Promise.all([appearMessagePromise, openReportPromise]) - .then(() => { - console.debug('[E2E] Test completed successfully at:', Date.now()); - console.debug('[E2E] Total test duration:', Date.now() - startTestTime, 'ms'); - E2EClient.submitTestDone(); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - - Performance.subscribeToMeasurements((entry) => { - console.debug(`[E2E] Performance entry captured: ${entry.name} at ${entry.startTime}, duration: ${entry.duration} ms`); - - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug('[E2E] Sidebar loaded, navigating to a report at:', Date.now()); - const startNavigateTime = Date.now(); - Performance.markStart(CONST.TIMING.OPEN_REPORT); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - console.debug('[E2E] Navigation to report took:', Date.now() - startNavigateTime, 'ms'); - return; - } - - if (entry.name === CONST.TIMING.OPEN_REPORT && !hasNavigatedToLinkedMessage) { - console.debug('[E2E] Navigating to the linked report action at:', Date.now()); - const startLinkedNavigateTime = Date.now(); - hasNavigatedToLinkedMessage = true; // Set flag to prevent duplicate navigation - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(linkedReportID, linkedReportActionID)); - console.debug('[E2E] Navigation to linked report took:', Date.now() - startLinkedNavigateTime, 'ms'); - - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name, - metric: entry.duration, - unit: 'ms', - }); - - openReportResolve(); - } - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/tests/moneyRequestTest.e2e.ts b/src/libs/E2E/tests/moneyRequestTest.e2e.ts deleted file mode 100644 index b50b7fe34693..000000000000 --- a/src/libs/E2E/tests/moneyRequestTest.e2e.ts +++ /dev/null @@ -1,76 +0,0 @@ -import Config from 'react-native-config'; -import type {NativeConfig} from 'react-native-config'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import E2EClient from '@libs/E2E/client'; -import {tap, waitForElement, waitForEvent, waitForTextInputValue} from '@libs/E2E/interactions'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import CONST from '@src/CONST'; -import {makeClearCommand} from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; - -const test = (config: NativeConfig) => { - // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for money request'); - - const name = getConfigValueOrThrow('name', config); - - E2ELogin().then((neededLogin) => { - if (neededLogin) { - return waitForAppLoaded().then(() => - // we don't want to submit the first login to the results - E2EClient.submitTestDone(), - ); - } - - console.debug('[E2E] Logged in, getting money request metrics and submitting them…'); - - waitForEvent(CONST.TIMING.SIDEBAR_LOADED) - .then(() => tap('floating-action-button')) - .then(() => waitForElement('create-expense')) - .then(() => tap('create-expense')) - .then(() => waitForEvent(CONST.TIMING.OPEN_CREATE_EXPENSE)) - .then((entry) => { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} - Open Manual Tracking`, - metric: entry.duration, - unit: 'ms', - }); - }) - .then(() => waitForElement('manual')) - .then(() => tap('manual')) - .then(() => E2EClient.sendNativeCommand(makeClearCommand())) - .then(() => tap('button_2')) - .then(() => waitForTextInputValue('2', 'moneyRequestAmountInput')) - .then(() => tap('next-button')) - .then(() => waitForEvent(CONST.TIMING.OPEN_CREATE_EXPENSE_CONTACT)) - .then((entry) => { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} - Open Contacts`, - metric: entry.duration, - unit: 'ms', - }); - }) - .then(() => waitForElement('+66 65 490 0617')) - .then(() => tap('+66 65 490 0617')) - .then(() => waitForEvent(CONST.TIMING.OPEN_CREATE_EXPENSE_APPROVE)) - .then((entry) => { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} - Open Create`, - metric: entry.duration, - unit: 'ms', - }); - }) - .then(() => { - console.debug('[E2E] Test completed successfully, exiting…'); - E2EClient.submitTestDone(); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts deleted file mode 100644 index 4fd2b26e63c8..000000000000 --- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts +++ /dev/null @@ -1,100 +0,0 @@ -import Config from 'react-native-config'; -import type {NativeConfig} from 'react-native-config'; -import * as E2EGenericPressableWrapper from '@components/Pressable/GenericPressable/index.e2e'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import E2EClient from '@libs/E2E/client'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Performance from '@libs/Performance'; -import CONST from '@src/CONST'; - -const test = (config: NativeConfig) => { - // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for new search router'); - - const name = getConfigValueOrThrow('name', config); - - E2ELogin().then((neededLogin: boolean): Promise | undefined => { - if (neededLogin) { - return waitForAppLoaded().then(() => - // we don't want to submit the first login to the results - E2EClient.submitTestDone(), - ); - } - - console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); - - const [openSearchRouterPromise, openSearchRouterResolve] = getPromiseWithResolve(); - const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); - - Promise.all([openSearchRouterPromise, loadSearchOptionsPromise]).then(() => { - console.debug(`[E2E] Submitting!`); - - E2EClient.submitTestDone(); - }); - - Performance.subscribeToMeasurements((entry) => { - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - const props = E2EGenericPressableWrapper.getPressableProps('searchButton'); - if (!props) { - console.debug('[E2E] Search button not found, failing test!'); - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - error: 'Search button not found', - name: `${name} Open Search Router TTI`, - }).then(() => E2EClient.submitTestDone()); - return; - } - if (!props.onPress) { - console.debug('[E2E] Search button found but onPress prop was not present, failing test!'); - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - error: 'Search button found but onPress prop was not present', - name: `${name} Open Search Router TTI`, - }).then(() => E2EClient.submitTestDone()); - return; - } - // Open the search router - props.onPress(); - } - - if (entry.name === CONST.TIMING.OPEN_SEARCH) { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} Open Search Router TTI`, - metric: entry.duration, - unit: 'ms', - }) - .then(() => { - openSearchRouterResolve(); - console.debug('[E2E] Done with search, exiting…'); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - } - - if (entry.name === CONST.TIMING.LOAD_SEARCH_OPTIONS) { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} Load Search Options`, - metric: entry.duration, - unit: 'ms', - }) - .then(() => { - loadSearchOptionsResolve(); - console.debug('[E2E] Done with loading search options, exiting…'); - }) - .catch((err) => { - console.debug('[E2E] Error while submitting test results:', err); - }); - } - - console.debug(`[E2E] Submitting!`); - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts deleted file mode 100644 index 65c91309ff69..000000000000 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type {NativeConfig} from 'react-native-config'; -import Config from 'react-native-config'; -import {scheduleOnUI} from 'react-native-worklets'; -import E2ELogin from '@libs/E2E/actions/e2eLogin'; -import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; -import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; -import E2EClient from '@libs/E2E/client'; -import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; -import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; -import {getRerenderCount, resetRerenderCount} from '@pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e'; -import {onSubmitAction} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import {makeBackspaceCommand, makeTypeTextCommand} from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; - -const test = (config: NativeConfig) => { - // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for typing'); - - const reportID = getConfigValueOrThrow('reportID', config); - const message = getConfigValueOrThrow('message', config); - const name = getConfigValueOrThrow('name', config); - - E2ELogin().then((neededLogin) => { - if (neededLogin) { - return waitForAppLoaded().then(() => - // we don't want to submit the first login to the results - E2EClient.submitTestDone(), - ); - } - - console.debug('[E2E] Logged in, getting typing metrics and submitting them…'); - - const [renderTimesPromise, renderTimesResolve] = getPromiseWithResolve(); - const [messageSentPromise, messageSentResolve] = getPromiseWithResolve(); - - Promise.all([renderTimesPromise, messageSentPromise]).then(() => { - console.debug(`[E2E] Submitting!`); - - E2EClient.submitTestDone(); - }); - - Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SEND_MESSAGE) { - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} Message sent`, - metric: entry.duration, - unit: 'ms', - }).then(messageSentResolve); - return; - } - - if (entry.name !== CONST.TIMING.SIDEBAR_LOADED) { - return; - } - - console.debug(`[E2E] Sidebar loaded, navigating to a report…`); - // Crowded Policy (Do Not Delete) Report, has a input bar available: - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - - // Wait until keyboard is visible (so we are focused on the input): - waitForKeyboard().then(() => { - console.debug(`[E2E] Keyboard visible, typing…`); - E2EClient.sendNativeCommand(makeBackspaceCommand()) - .then(() => { - resetRerenderCount(); - return Promise.resolve(); - }) - .then(() => E2EClient.sendNativeCommand(makeTypeTextCommand('A'))) - .then( - () => - new Promise((resolve) => { - setTimeout(() => { - const rerenderCount = getRerenderCount(); - - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: `${name} Composer typing rerender count`, - metric: rerenderCount, - unit: 'renders', - }) - .then(renderTimesResolve) - .then(resolve); - }, 3000); - }), - ) - .then(() => E2EClient.sendNativeCommand(makeBackspaceCommand())) - .then(() => E2EClient.sendNativeCommand(makeTypeTextCommand(message))) - .then(() => scheduleOnUI(onSubmitAction)) - .catch((error) => { - console.error('[E2E] Error while test', error); - E2EClient.submitTestDone(); - }); - }); - }); - }); -}; - -export default test; diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts deleted file mode 100644 index 8640c76e631e..000000000000 --- a/src/libs/E2E/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type {TEST_NAMES} from 'tests/e2e/config'; -import type {ValueOf} from 'type-fest'; -import type E2EConfig from '../../../tests/e2e/config'; - -type SigninParams = { - email?: string; -}; - -type IsE2ETestSession = () => boolean; - -type NetworkCacheEntry = { - url: string; - options: RequestInit; - status: number; - statusText: string; - headers: Record; - body: string; -}; - -type NetworkCacheMap = Record< - string, // hash - NetworkCacheEntry ->; - -type TestConfig = { - name: ValueOf; - [key: string]: string | {autoFocus: boolean}; -}; - -type Test = (config: TestConfig) => void; - -type TestModule = {default: Test}; - -type Tests = Record, Test>; - -type Unit = 'ms' | 'MB' | '%' | 'renders' | 'FPS'; - -type TestResult = { - /** Name of the test */ - name: string; - - /** The branch where test were running */ - branch?: string; - - /** The numeric value of the measurement */ - metric?: number; - - /** Optional, if set indicates that the test run failed and has no valid results. */ - error?: string; - - /** - * Whether error is critical. If `true`, then server will be stopped and `e2e` tests will fail. Otherwise will simply log a warning. - * Default value is `true` - */ - isCritical?: boolean; - - /** The unit of the measurement */ - unit?: Unit; -}; - -export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig, TestResult, TestModule, Tests, Unit}; diff --git a/src/libs/E2E/utils/LaunchArgs.ts b/src/libs/E2E/utils/LaunchArgs.ts deleted file mode 100644 index 4e452d766eff..000000000000 --- a/src/libs/E2E/utils/LaunchArgs.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {LaunchArguments} from 'react-native-launch-arguments'; - -type ExpectedArgs = { - mockNetwork?: boolean; -}; -const LaunchArgs = LaunchArguments.value(); - -export default LaunchArgs; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts deleted file mode 100644 index 9a10e8d2e068..000000000000 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ /dev/null @@ -1,211 +0,0 @@ -import {DeviceEventEmitter} from 'react-native'; -import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types'; - -const LOG_TAG = `[E2E][NetworkInterceptor]`; -// Requests with these headers will be ignored: -const IGNORE_REQUEST_HEADERS = ['X-E2E-Server-Request']; - -let globalResolveIsNetworkInterceptorInstalled: () => void; -let globalRejectIsNetworkInterceptorInstalled: (error: Error) => void; -const globalIsNetworkInterceptorInstalledPromise = new Promise((resolve, reject) => { - globalResolveIsNetworkInterceptorInstalled = resolve; - globalRejectIsNetworkInterceptorInstalled = reject; -}); -let networkCache: NetworkCacheMap | null = null; - -/** - * The headers of a fetch request can be passed as an array of tuples or as an object. - * This function converts the headers to an object. - */ -function getFetchRequestHeadersAsObject(fetchRequest: RequestInit): Record { - const headers: Record = {}; - if (Array.isArray(fetchRequest.headers)) { - for (const [key, value] of fetchRequest.headers) { - headers[key] = value; - } - } else if (typeof fetchRequest.headers === 'object') { - for (const [key, value] of Object.entries(fetchRequest.headers)) { - headers[key] = value as string; - } - } - return headers; -} - -/** - * This function extracts the RequestInit from the arguments of fetch. - * It is needed because the arguments can be passed in different ways. - */ -function fetchArgsGetRequestInit(args: Parameters): RequestInit { - const [firstArg, secondArg] = args; - if (typeof firstArg === 'string' || (typeof firstArg === 'object' && firstArg instanceof URL)) { - if (secondArg == null) { - return {}; - } - return secondArg; - } - return firstArg; -} - -/** - * This function extracts the url from the arguments of fetch. - */ -function fetchArgsGetUrl(args: Parameters): string { - const [firstArg] = args; - if (typeof firstArg === 'string') { - return firstArg; - } - if (typeof firstArg === 'object' && firstArg instanceof URL) { - return firstArg.href; - } - if (typeof firstArg === 'object' && firstArg instanceof Request) { - return firstArg.url; - } - throw new Error('Could not get url from fetch args'); -} - -/** - * This function transforms a NetworkCacheEntry (internal representation) to a (fetch) Response. - */ -function networkCacheEntryToResponse({headers, status, statusText, body}: NetworkCacheEntry): Response { - // Transform headers to Headers object: - const newHeaders = new Headers(); - for (const [key, value] of Object.entries(headers)) { - newHeaders.append(key, value); - } - - return new Response(body, { - status, - statusText, - headers: newHeaders, - }); -} - -/** - * This function hashes the arguments of fetch. - */ -function hashFetchArgs(args: Parameters) { - const url = fetchArgsGetUrl(args); - const options = fetchArgsGetRequestInit(args); - const headers = getFetchRequestHeadersAsObject(options); - // Note: earlier we were using the body value as well, however - // the body for the same request might be different due to including - // times or app versions. - return `${url}${JSON.stringify(headers)}`; -} - -let activeRequestsCount = 0; - -const ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT = 'activeRequestsQueueIsEmpty'; - -/** - * Assures that ongoing network requests are empty. **Highly desirable** to call this function before closing the app. - * Otherwise if some requests are persisted - they will be executed on the next app start. And it can lead to a situation - * where we can have `N * M` requests (where `N` is the number of app run per test and `M` is the number of test suites) - * and such big amount of requests can lead to a situation, where first app run (in test suite to cache network requests) - * may be blocked by spinners and lead to unbelievable big time execution, which eventually will be bigger than timeout and - * will lead to a test failure. - */ -function waitForActiveRequestsToBeEmpty(): Promise { - console.debug('Waiting for requests queue to be empty...', activeRequestsCount); - - if (activeRequestsCount === 0) { - return Promise.resolve(); - } - - return new Promise((resolve) => { - const subscription = DeviceEventEmitter.addListener(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT, () => { - subscription.remove(); - resolve(); - }); - }); -} - -/** - * Install a network interceptor by overwriting the global fetch function: - * - Overwrites fetch globally with a custom implementation - * - For each fetch request we cache the request and the response - * - The cache is send to the test runner server to persist the network cache in between sessions - * - On e2e test start the network cache is requested and loaded - * - If a fetch request is already in the NetworkInterceptors cache instead of making a real API request the value from the cache is used. - */ -export default function installNetworkInterceptor( - getNetworkCache: () => Promise, - updateNetworkCache: (networkCache: NetworkCacheMap) => Promise, - shouldReturnRecordedResponse: boolean, -) { - console.debug(LOG_TAG, 'installing with shouldReturnRecordedResponse:', shouldReturnRecordedResponse); - const originalFetch = global.fetch; - - if (networkCache == null && shouldReturnRecordedResponse) { - console.debug(LOG_TAG, 'fetching network cache …'); - getNetworkCache() - .then((newCache) => { - networkCache = newCache; - globalResolveIsNetworkInterceptorInstalled(); - console.debug(LOG_TAG, 'network cache fetched!'); - }, globalRejectIsNetworkInterceptorInstalled) - .catch(globalRejectIsNetworkInterceptorInstalled); - } else { - networkCache = {}; - globalResolveIsNetworkInterceptorInstalled(); - } - - global.fetch = async (...args: Parameters) => { - const options = fetchArgsGetRequestInit(args); - const headers = getFetchRequestHeadersAsObject(options); - const url = fetchArgsGetUrl(args); - - // Check if headers contain any of the ignored headers, or if react native metro server: - if (IGNORE_REQUEST_HEADERS.some((header) => headers[header] != null) || url.includes('8081')) { - return originalFetch(...args); - } - - await globalIsNetworkInterceptorInstalledPromise; - - const hash = hashFetchArgs(args); - const cachedResponse = networkCache?.[hash]; - if (shouldReturnRecordedResponse && cachedResponse != null) { - const response = networkCacheEntryToResponse(cachedResponse); - console.debug(LOG_TAG, 'Returning recorded response for url:', url); - return Promise.resolve(response); - } - if (shouldReturnRecordedResponse) { - console.debug('!!! Missed cache hit for url:', url); - } - - activeRequestsCount++; - - return originalFetch(...args) - .then(async (res) => { - if (networkCache != null) { - const body = await res.clone().text(); - networkCache[hash] = { - url, - options, - body, - headers: getFetchRequestHeadersAsObject(options), - status: res.status, - statusText: res.statusText, - }; - console.debug(LOG_TAG, 'Updating network cache for url:', url); - // Send the network cache to the test server: - return updateNetworkCache(networkCache).then(() => res); - } - return res; - }) - .then((res) => { - console.debug(LOG_TAG, 'Network cache updated!'); - return res; - }) - .finally(() => { - console.debug('Active requests count:', activeRequestsCount); - - activeRequestsCount--; - - if (activeRequestsCount === 0) { - DeviceEventEmitter.emit(ACTIVE_REQUESTS_QUEUE_IS_EMPTY_EVENT); - } - }); - }; -} -export {waitForActiveRequestsToBeEmpty}; diff --git a/src/libs/E2E/utils/getConfigValueOrThrow.ts b/src/libs/E2E/utils/getConfigValueOrThrow.ts deleted file mode 100644 index a694d6709ed6..000000000000 --- a/src/libs/E2E/utils/getConfigValueOrThrow.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Config from 'react-native-config'; - -/** - * Gets a config value or throws an error if the value is not defined. - */ -export default function getConfigValueOrThrow(key: string, config = Config): string { - const value = config[key]; - if (value == null) { - throw new Error(`Missing config value for ${key}`); - } - return value; -} diff --git a/src/libs/E2E/utils/getPromiseWithResolve.ts b/src/libs/E2E/utils/getPromiseWithResolve.ts deleted file mode 100644 index fd49cd82c513..000000000000 --- a/src/libs/E2E/utils/getPromiseWithResolve.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function getPromiseWithResolve(): [Promise, (value?: T) => void] { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let resolveFn = (_value?: T) => {}; - const promise = new Promise((resolve) => { - resolveFn = resolve; - }); - - return [promise, resolveFn]; -} diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx index 457aac34e7bd..b867ce540e83 100644 --- a/src/libs/Performance.tsx +++ b/src/libs/Performance.tsx @@ -6,7 +6,6 @@ import {Alert, InteractionManager} from 'react-native'; import performance, {PerformanceObserver, setResourceLoggingEnabled} from 'react-native-performance'; import type {PerformanceEntry, PerformanceMark, PerformanceMeasure} from 'react-native-performance'; import CONST from '@src/CONST'; -import isE2ETestSession from './E2E/isE2ETestSession'; import getComponentDisplayName from './getComponentDisplayName'; import canCapturePerformanceMetrics from './Metrics'; @@ -50,10 +49,7 @@ function measureTTI(endMark?: string): void { requestAnimationFrame(() => { measureFailSafe('TTI', 'nativeLaunchStart', endMark); - // We don't want an alert to show: - // - on builds with performance metrics collection disabled by a feature flag - // - e2e test sessions - if (!canCapturePerformanceMetrics() || isE2ETestSession()) { + if (!canCapturePerformanceMetrics()) { return; } diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts index 0837b8d45bb3..be2a1480ccec 100644 --- a/src/libs/actions/QueuedOnyxUpdates.ts +++ b/src/libs/actions/QueuedOnyxUpdates.ts @@ -32,7 +32,7 @@ function flushQueue(): Promise { // Clear queue immediately to prevent race conditions with new updates during Onyx processing queuedOnyxUpdates = []; - if (!currentAccountID && !CONFIG.IS_TEST_ENV && !CONFIG.E2E_TESTING) { + if (!currentAccountID && !CONFIG.IS_TEST_ENV) { const preservedKeys = new Set([ ONYXKEYS.NVP_TRY_NEW_DOT, ONYXKEYS.NVP_TRY_FOCUS_MODE, diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx deleted file mode 100644 index d9a5b4aced07..000000000000 --- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, {useCallback, useRef} from 'react'; -import type {LayoutChangeEvent} from 'react-native'; -import {Keyboard} from 'react-native'; -import E2EClient from '@libs/E2E/client'; -import type {ComposerRef} from '@pages/inbox/report/ReportActionCompose/ReportActionCompose'; -import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions'; -import ComposerWithSuggestions from './ComposerWithSuggestions'; - -let rerenderCount = 0; -const getRerenderCount = () => rerenderCount; -const resetRerenderCount = () => { - rerenderCount = 0; -}; - -function IncrementRenderCount() { - rerenderCount += 1; - return null; -} - -function ComposerWithSuggestionsE2e({ref, ...props}: ComposerWithSuggestionsProps) { - 'use no memo'; - - // we rely on waterfall rendering in react, so we intentionally disable compiler - // for this component. This file is only used for e2e tests, so it's okay to - // disable compiler for this file. - - const textInputRef = useRef(null); - const hasFocusBeenRequested = useRef(false); - const onLayout = useCallback((event: LayoutChangeEvent) => { - const testConfig = E2EClient.getCurrentActiveTestConfig(); - if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && !testConfig?.reportScreen.autoFocus) { - return; - } - const canRequestFocus = event.nativeEvent.layout.width > 0 && !hasFocusBeenRequested.current; - if (!canRequestFocus) { - return; - } - - hasFocusBeenRequested.current = true; - - const setFocus = () => { - console.debug('[E2E] Requesting focus for ComposerWithSuggestions'); - if (!(textInputRef && 'current' in textInputRef)) { - console.error('[E2E] textInputRef is not available, failed to focus'); - return; - } - - textInputRef.current?.focus(true); - - setTimeout(() => { - // and actually let's verify that the keyboard is visible - if (Keyboard.isVisible()) { - return; - } - - textInputRef.current?.blur(); - setFocus(); - // 1000ms is enough time for any keyboard to open - }, 1_000); - }; - - // Simulate user behavior and don't set focus immediately - setTimeout(setFocus, 2_000); - }, []); - - return ( - { - textInputRef.current = composerRef; - - if (typeof ref === 'function') { - ref(composerRef); - } - }} - onLayout={onLayout} - > - {/* Important: - this has to be a child, as this container might not - re-render while the actual ComposerWithSuggestions will. - */} - - - ); -} - -export default ComposerWithSuggestionsE2e; -export {getRerenderCount, resetRerenderCount}; diff --git a/tests/e2e/.env.e2e b/tests/e2e/.env.e2e deleted file mode 100644 index a6611ad2ff9d..000000000000 --- a/tests/e2e/.env.e2e +++ /dev/null @@ -1,3 +0,0 @@ -E2E_BRANCH=main -E2E_TESTING=true -CAPTURE_METRICS=true diff --git a/tests/e2e/.env.e2edelta b/tests/e2e/.env.e2edelta deleted file mode 100644 index a5d7a96d0788..000000000000 --- a/tests/e2e/.env.e2edelta +++ /dev/null @@ -1,3 +0,0 @@ -E2E_BRANCH=delta -E2E_TESTING=true -CAPTURE_METRICS=true diff --git a/tests/e2e/ADDING_TESTS.md b/tests/e2e/ADDING_TESTS.md deleted file mode 100644 index 5986e21f2fc8..000000000000 --- a/tests/e2e/ADDING_TESTS.md +++ /dev/null @@ -1,116 +0,0 @@ -# Adding new E2E Tests - -## Creating a new test - -Tests are executed on device, inside the app code. - -The tests are located in `src/libs/E2E/tests`. - -You have to register your test in the `e2e/config`, see the following diff: - -### e2e/config.js - -First, set the name for you test. - -```diff - const OUTPUT_DIR = 'e2e/.results'; - // add your test name here … - const TEST_NAMES = { - AppStartTime: 'App start time', -+ AnotherTest: 'Another test', - }; -``` - -Then you provide a configuration for you test. At least, you need to -set a name property for the config. You can, however, add any other values -that you might need to pass to the test running inside the app: - -```diff - module.exports = { - // … - TESTS_CONFIG: { -+ [TEST_NAMES.AnotherTest]: { -+ name: TEST_NAMES.AnotherTest, -+ -+ // ... any additional config you might need -+ }, - }, -``` - -### Create the actual test - -We created a new test file in `src/libs/E2E/tests/`. Typically, the -tests ends on `.e2e.js`, so we can distinguish it from the other tests. - -Inside this test, we write logic that gets executed in the app. You can basically do -anything here, like connecting to onyx, calling APIs, navigating. - -There are some common actions that are common among different test cases: - -- `src/libs/E2E/actions/e2eLogin.ts` - Log a user into the app. - -The test will be called once the app is ready, which mean you can immediately start. -Your test is expected to default export its test function. - -An example test, which test the time it takes to navigate to a screen might looks like this: - -```js -// new file in src/libs/E2E/tests/anotherTest.e2e.js - -import Navigation from "src/libs/Navigation/Navigation"; -import Performance from "src/libs/Performance"; -import E2EClient from "./client.js"; - -const test = () => { - const firstReportIDInList = // ... some logic to get a report - - performance.markStart("navigateToReport"); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(firstReportIDInList)); - - // markEnd will be called in the Screen's implementation - Performance.subscribeToMeasurements((measurement) => { - if (measurement.name !== "navigateToReport") { - return; - } - - E2EClient.submitTestResults({ - name: "Navigate to report", - metric: measurement.duration, - unit: 'ms', - }).then(E2EClient.submitTestDone) - }); - -}; - -export default test; -``` - -### Last step: register the test in the e2e react native entry - -In `src/lib/E2E/reactNativeLaunchingTest.ts` you have to add your newly created -test file: - -```diff - // import your test here, define its name and config first in e2e/config.js - const tests = { - [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, -+ [E2EConfig.TEST_NAMES.AnotherTest]: require('./tests/anotherTest.e2e').default, - }; -``` - -Done! When you now start the test runner, your new test will be executed as well. - -## Quickly test your test - -> [!TIP] -> You can only run a specific test by specifying the `--includes` flag: -> ```sh -> npm run test:e2e:dev -- --includes "My new test name" -> ``` - -It is recommended to run a debug build of the e2e tests first to iterate quickly on your test. Follow the explanation in the [README](./README.md) to create a debug build. - -## Debugging your test - -You can use regular console statements to debug your test. The output will be visible -in logcat. I recommend opening the android studio logcat window and filter for `ReactNativeJS` to see the output you'd otherwise typically see in your metro bundler instance. diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index 88676e73f16d..000000000000 --- a/tests/e2e/README.md +++ /dev/null @@ -1,201 +0,0 @@ -# E2E performance regression tests - -This directory contains the scripts and configuration files for running the -performance regression tests. These tests are called E2E tests because they -run the actual app on a real device (physical or emulated). - -![Example of a e2e test run](https://raw.githubusercontent.com/hannojg/expensify-app/5f945c25e2a0650753f47f3f541b984f4d114f6d/e2e/example.gif) - - -## Available CLI options - -The tests can be run with the following CLI options: - -- `--config`: Extend/Overwrite the default config with your values, e.g. `--config config.local.ts` -- `--includes`: Expects a string/regexp to filter the tests to run, e.g. `--includes "login|signup"` - -## Running the tests on your machine - -You have two options when running the e2e tests: - -1. Run a debug build of the app (useful when developing a test) -2. Run two (e2e) release builds against each other (useful to test performance regression and the suite as a whole) - -### Running a debug build - -1. You need to create a debug build of the app that's configured with some build flags to enable e2e testing. -The build flags should be specified in your `./.env` file. You can use the `./tests/e2e/.env.e2e` file as a template: - -```sh -cp ./tests/e2e/.env.e2e .env -``` - -> [!IMPORTANT] -> There are some non-public environment variables that you still have to add to the `.env` file. Ask on slack for the values (cc @vit, @andrew, @hanno gödecke). - -2. Create a new android build like you usually would: - -```sh -npm run android -``` - -3. We need to modify the app entry to point to the one for the tests. Therefore rename `./index.js` to `./appIndex.js` temporarily. - -4. Temporarily add to the `package.json` a `main` field pointing to the e2e entry file: - - ```diff - { - "private": true, -+ "main": "src/libs/E2E/reactNativeLaunchingTest.ts" - } - ``` - -5. You can now run the tests. This command will invoke the test runner: - -```sh -npm run test:e2e:dev -``` - -### Running two release builds - -The e2e tests are meant to detect performance regressions. For that we need to compare two builds against each other. On the CI system this is e.g. the latest release build (baseline) VS the currently merged PR (compare). - -You need to build the two apps first. Note that the two apps will be installed on the same device at the same time, so both apps have a different package name. Therefor, we have special build types for the e2e tests. - -1. Create a new android build for the baseline: - -> [!IMPORTANT] -> There are some non-public environment variables that you still have to add to the `./tests/e2e/.env.e2e` and `./tests/e2e/.env.e2edelta` file. Ask on slack for the values (cc @vit, @andrew, @hanno gödecke). - -```sh -npm run android-build-e2e -``` - -2. Create a new android build for the compare: - -```sh -npm run android-build-e2edelta -``` - -3. Run the tests: - -```sh -npm run test:e2e -``` - - -## Performance regression testing - -The output of the tests is a set of performance metrics (see video above). -The test will tell you if the performance significantly worsened for any test case. - -For this to work you need a baseline you test against. The baseline is set by default -to the `main` branch. - -The test suite will run each test-case twice, once on the baseline, and then on the branch -you are currently on. - -It will run the tests of a test case multiple time to average out the results. - -## Adding tests - -To add a test checkout the [designed guide](tests/e2e/ADDING_TESTS.md). - -## Structure - -For the test suite, no additional tooling was used. It is made of the following -components: - -- The tests themselves : - - The tests are located in `src/libs/E2E/tests` - - As opposed to other test frameworks, the tests are _inside the app_, and execute logic using app code (e.g. `navigationRef.navigate('Signin')`) - - For the tests there is a custom entry for react native, located in `src/libs/E2E/reactNativeLaunchingTest.ts` - -- The test runner: - - Orchestrates the test suite. - - Runs the app with the tests on a device - - Responsible for gathering and comparing results - - Located in `e2e/testRunner.ts`. - -- Test server: - - A nodeJS application that starts an HTTP server. - - Receives test results from the app. - - Located in `e2e/server`. - -- Client: - - Client-side code (app) for communication with the test server. - - Located in `src/libs/E2E/client.ts`. - - -## How a test gets executed - -There exists a custom android entry point for the app, which is used for the e2e tests. -The entry file used is `src/libs/E2E/reactNativeEntry.ts`, and here we can add our test case. - -The test case should only execute its test once. The _test runner_ is responsible for running the -test multiple time to average out the results. - -Any results of the test (which is usually a duration, like "Time it took to reopen chat", or "TTI") should be -submitted to the test server using the client: - -```js -import E2EClient from './client'; - -// ... run you test logic -const someDurationWeCollected = // ... - -E2EClient.submitTestResults({ - name: 'My test name', - metric: someDurationWeCollected, - unit: 'ms', -}); -``` - -Submitting test results doesn't automatically finish the test. This enables you do submit multiple test results -from one test (e.g. measuring multiple things at the same time). - -To finish a test call `E2EClient.submitTestDone()`. - -## Network calls - -Network calls can add a variance to the test results. To mitigate this in the past we used to provide mocks for the API -calls. However, this is not a realistic scenario, as we want to test the app in a realistic environment. - -Now we have a module called `NetworkInterceptor`. The interceptor will intercept all network calls and will -cache the request and response. The next time the same request is made, it will return the cached response. - -When writing a test you usually don't need to care about this, as the interceptor is enabled by default. -However, look out for "!!! Missed cache hit for url" logs when developing your test. This can indicate a bug -with the NetworkInterceptor where a request should have been cached but wasn't (which would introduce variance in your test!). - - -## Android specifics - -The tests are designed to run on android (although adding support for iOS should be easy to add). -To test under realistic conditions during the tests a release build is used. - -However, to be able to call our local HTTP test server, we need to allow -[cleartext http traffic](https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted). -Therefore, a customized release build type is needed, which is called `e2eRelease`. This build type has clear -text traffic enabled but works otherwise just like a release build. - -In addition to that, another entry file will be used (instead of `index.js`). The entry file used is -`src/libs/E2E/reactNativeEntry.ts`. By using a custom entry file we avoid bundling any e2e testing code -into the actual release app. - -For the app to detect that it is currently running e2e tests, an environment variable called `E2E_TESTING=true` must -be set. There is a custom environment file in `e2e/.env.e2e` that contains the env setup needed. The build automatically -picks this file for configuration. - -It can be useful to debug the app while running the e2e tests (to catch errors during development of a test). -You can simply add the `debuggable true` property to the `e2eRelease` buildType config in `android/app/build.gradle`. -Then rebuild the app. You can now monitor the app's logs using `logcat` (`adb logcat | grep "ReactNativeJS"`). - -## Test the accuracy of the test suite - -If you run the tests on the same branch, the result should ideally be a difference of 0%. However, as the tests -get executed on an emulator, there will be some variance. This variance is mitigated by using statistical tools -such as [z-test](https://en.wikipedia.org/wiki/Z-test). However, when running on the same branch, the results -should be below 5% of change. -You might want to tweak the values in `e2e/config.js` to adjust those values. - diff --git a/tests/e2e/TestSpec.yml b/tests/e2e/TestSpec.yml deleted file mode 100644 index 563a7eb4ce0e..000000000000 --- a/tests/e2e/TestSpec.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: 0.1 - -android_test_host: amazon_linux_2 - -phases: - install: - commands: - - export FLASHLIGHT_BINARY_PATH=$DEVICEFARM_TEST_PACKAGE_PATH/zip/bin - # Note: Node v16 is the latest supported version of node for AWS Device Farm - # using v20 will not work! - - devicefarm-cli use node 16 - - node -v - - # Reverse ports using AWS magic - - PORT=4723 - - IP_ADDRESS=$(ip -4 addr show eth0 | grep -Po "(?<=inet\s)\d+(\.\d+){3}") - - reverse_values="{\"ip_address\":\"$IP_ADDRESS\",\"local_port\":\"$PORT\",\"remote_port\":\"$PORT\"}" - - 'curl -H "Content-Type: application/json" -X POST -d "$reverse_values" http://localhost:31007/reverse_forward_tcp' - - adb reverse tcp:$PORT tcp:$PORT - - test: - commands: - - cd zip - - node testRunner.ts -- --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk - -artifacts: - - $WORKING_DIRECTORY diff --git a/tests/e2e/compare/compare.ts b/tests/e2e/compare/compare.ts deleted file mode 100644 index 95363f28c527..000000000000 --- a/tests/e2e/compare/compare.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type {Unit} from '@libs/E2E/types'; -import type {Stats} from '../measure/math'; -import getStats from '../measure/math'; -import * as math from './math'; -import type {Entry} from './output/console'; -import printToConsole from './output/console'; -import writeToMarkdown from './output/markdown'; - -type Metric = Record; - -/* - * base implementation from: https://github.com/callstack/reassure/blob/main/packages/reassure-compare/src/compare.ts - * This module reads from the baseline and compare files and compares the results. - * It has a few different output formats: - * - console: prints the results to the console - * - markdown: Writes the results in markdown format to a file - */ - -/** - * Probability threshold for considering given difference significant. - */ -const PROBABILITY_CONSIDERED_SIGNIFICANCE = 0.02; - -/** - * Duration threshold (in ms) for treating given difference as significant. - * - * This is additional filter, in addition to probability threshold above. - * Too small duration difference might be result of measurement grain of 1 ms. - */ -const DURATION_DIFF_THRESHOLD_SIGNIFICANCE = 100; - -const LowerIsBetter: Record = { - ms: true, - MB: true, - // eslint-disable-next-line @typescript-eslint/naming-convention - '%': true, - renders: true, - FPS: false, -}; - -function buildCompareEntry(name: string, compare: Stats, baseline: Stats, unit: Unit): Entry { - const diff = compare.mean - baseline.mean; - const relativeDurationDiff = diff / baseline.mean; - - const z = math.computeZ(baseline.mean, baseline.stdev, compare.mean, compare.runs); - const prob = math.computeProbability(z); - - const isDurationDiffOfSignificance = prob < PROBABILITY_CONSIDERED_SIGNIFICANCE && Math.abs(diff) >= DURATION_DIFF_THRESHOLD_SIGNIFICANCE; - - return { - unit, - name, - baseline, - current: compare, - diff, - relativeDurationDiff: LowerIsBetter[unit] ? relativeDurationDiff : -relativeDurationDiff, - isDurationDiffOfSignificance, - }; -} - -/** - * Compare results between baseline and current entries and categorize. - */ -function compareResults(baselineEntries: Metric | string, compareEntries: Metric | string = baselineEntries, metricForTest: Record = {}) { - // Unique test scenario names - const baselineKeys = Object.keys(baselineEntries ?? {}); - const names = Array.from(new Set([...baselineKeys])); - - const compared: Entry[] = []; - - if (typeof compareEntries !== 'string' && typeof baselineEntries !== 'string') { - for (const name of names) { - const current = compareEntries[name]; - const baseline = baselineEntries[name]; - - const currentStats = getStats(baseline); - const deltaStats = getStats(current); - - if (baseline && current) { - compared.push(buildCompareEntry(name, deltaStats, currentStats, metricForTest[name])); - } - } - } - const significance = compared.filter((item) => item.isDurationDiffOfSignificance); - - const meaningless = compared.filter((item) => !item.isDurationDiffOfSignificance); - - return { - significance, - meaningless, - }; -} - -type Options = { - outputDir: string; - outputFormat: 'console' | 'markdown' | 'all'; - metricForTest: Record; - skippedTests: string[]; -}; - -export default (main: Metric | string, delta: Metric | string, {outputDir, outputFormat = 'all', metricForTest = {}, skippedTests}: Options) => { - // IMPORTANT NOTE: make sure you are passing the main/baseline results first, then the delta/compare results: - const outputData = compareResults(main, delta, metricForTest); - - if (outputFormat === 'console' || outputFormat === 'all') { - printToConsole(outputData, skippedTests); - } - - if (outputFormat === 'markdown' || outputFormat === 'all') { - return writeToMarkdown(outputDir, outputData, skippedTests); - } -}; -export {compareResults}; diff --git a/tests/e2e/compare/math.ts b/tests/e2e/compare/math.ts deleted file mode 100644 index 59a56dd3c842..000000000000 --- a/tests/e2e/compare/math.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Base implementation from: https://github.com/callstack/reassure/blob/main/packages/reassure-compare/src/compare.ts - */ - -/** - * Calculate z-score for given baseline and current performance results. - * - * Based on :: https://github.com/v8/v8/blob/master/test/benchmarks/csuite/compare-baseline.py - * - */ -const computeZ = (baselineMean: number, baselineStdev: number, currentMean: number, runs: number): number => { - if (baselineStdev === 0) { - return 1000; - } - - return Math.abs((currentMean - baselineMean) / (baselineStdev / Math.sqrt(runs))); -}; - -/** - * Compute statistical hypothesis probability based on z-score. - * - * Based on :: https://github.com/v8/v8/blob/master/test/benchmarks/csuite/compare-baseline.py - * - */ -const computeProbability = (z: number): number => { - // p 0.005: two sided < 0.01 - if (z > 2.575829) { - return 0; - } - - // p 0.010 - if (z > 2.326348) { - return 0.01; - } - - // p 0.015 - if (z > 2.170091) { - return 0.02; - } - - // p 0.020 - if (z > 2.053749) { - return 0.03; - } - - // p 0.025: two sided < 0.05 - if (z > 1.959964) { - return 0.04; - } - - // p 0.030 - if (z > 1.880793) { - return 0.05; - } - - // p 0.035 - if (z > 1.81191) { - return 0.06; - } - - // p 0.040 - if (z > 1.750686) { - return 0.07; - } - - // p 0.045 - if (z > 1.695397) { - return 0.08; - } - - // p 0.050: two sided < 0.10 - if (z > 1.644853) { - return 0.09; - } - - // p 0.100: two sided < 0.20 - if (z > 1.281551) { - return 0.1; - } - - // two sided p >= 0.20 - return 0.2; -}; - -export {computeZ, computeProbability}; diff --git a/tests/e2e/compare/output/console.ts b/tests/e2e/compare/output/console.ts deleted file mode 100644 index 9f58d96dad2e..000000000000 --- a/tests/e2e/compare/output/console.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type {Unit} from '@libs/E2E/types'; -import type {Stats} from '../../measure/math'; -import * as format from './format'; - -type Entry = { - name: string; - baseline: Stats; - current: Stats; - diff: number; - relativeDurationDiff: number; - isDurationDiffOfSignificance: boolean; - unit: Unit; -}; - -type Data = { - significance: Entry[]; - meaningless: Entry[]; - errors?: string[]; - warnings?: string[]; -}; - -const printRegularLine = (entry: Entry) => { - console.debug(` - ${entry.name}: ${format.formatMetricDiffChange(entry)}`); -}; - -/** - * Prints the result simply to console. - */ -export default (data: Data, skippedTests: string[]) => { - // No need to log errors or warnings as these were be logged on the fly - console.debug(''); - console.debug('❇️ Performance comparison results:'); - - console.debug('\n➡️ Significant changes to duration'); - for (const significance of data.significance) { - printRegularLine(significance); - } - - console.debug('\n➡️ Meaningless changes to duration'); - for (const meaningless of data.meaningless) { - printRegularLine(meaningless); - } - - console.debug(''); - - if (skippedTests.length > 0) { - console.debug(`⚠️ Some tests did not pass successfully, so some results are omitted from final report: ${skippedTests.join(', ')}`); - } -}; - -export type {Data, Entry}; diff --git a/tests/e2e/compare/output/format.ts b/tests/e2e/compare/output/format.ts deleted file mode 100644 index f00684cd5a01..000000000000 --- a/tests/e2e/compare/output/format.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Utility for formatting text for result outputs. - * from: https://github.com/callstack/reassure/blob/main/packages/reassure-compare/src/utils/format.ts - */ -import type {Entry} from './console'; - -const formatPercent = (value: number): string => { - const valueAsPercent = value * 100; - return `${valueAsPercent.toFixed(1)}%`; -}; - -const formatPercentChange = (value: number): string => { - const absValue = Math.abs(value); - - // Round to zero - if (absValue < 0.005) { - return '±0.0%'; - } - - return `${value >= 0 ? '+' : '-'}${formatPercent(absValue)}`; -}; - -const formatMetric = (duration: number, unit: string): string => `${duration.toFixed(3)} ${unit}`; - -const formatMetricChange = (value: number, unit: string): string => { - if (value > 0) { - return `+${formatMetric(value, unit)}`; - } - if (value < 0) { - return `${formatMetric(value, unit)}`; - } - return `0 ${unit}`; -}; - -const formatChange = (value: number): string => { - if (value > 0) { - return `+${value}`; - } - if (value < 0) { - return `${value}`; - } - return '0'; -}; - -const getDurationSymbols = (entry: Entry): string => { - if (!entry.isDurationDiffOfSignificance) { - if (entry.relativeDurationDiff > 0.15) { - return '🟡'; - } - if (entry.relativeDurationDiff < -0.15) { - return '🟢'; - } - return ''; - } - - if (entry.relativeDurationDiff > 0.33) { - return '🔴🔴'; - } - if (entry.relativeDurationDiff > 0.05) { - return '🔴'; - } - if (entry.relativeDurationDiff < -0.33) { - return '🟢🟢'; - } - if (entry.relativeDurationDiff < -0.05) { - return ' 🟢'; - } - - return ''; -}; - -const formatMetricDiffChange = (entry: Entry): string => { - const {baseline, current} = entry; - - let output = `${formatMetric(baseline.mean, entry.unit)} → ${formatMetric(current.mean, entry.unit)}`; - - if (baseline.mean !== current.mean) { - output += ` (${formatMetricChange(entry.diff, entry.unit)}, ${formatPercentChange(entry.relativeDurationDiff)})`; - } - - output += ` ${getDurationSymbols(entry)}`; - - return output; -}; - -export {formatPercent, formatPercentChange, formatMetric, formatMetricChange, formatChange, getDurationSymbols, formatMetricDiffChange}; diff --git a/tests/e2e/compare/output/markdown.ts b/tests/e2e/compare/output/markdown.ts deleted file mode 100644 index bc34f1f42615..000000000000 --- a/tests/e2e/compare/output/markdown.ts +++ /dev/null @@ -1,197 +0,0 @@ -// From: https://raw.githubusercontent.com/callstack/reassure/main/packages/reassure-compare/src/output/markdown.ts -import fs from 'node:fs/promises'; -import path from 'path'; -import type {Stats} from 'tests/e2e/measure/math'; -import * as Logger from '../../utils/logger'; -import type {Data, Entry} from './console'; -import * as format from './format'; -import markdownTable from './markdownTable'; - -const MAX_CHARACTERS_PER_FILE = 65536; -const FILE_SIZE_SAFETY_MARGIN = 1000; -const MAX_CHARACTERS_PER_FILE_WITH_SAFETY_MARGIN = MAX_CHARACTERS_PER_FILE - FILE_SIZE_SAFETY_MARGIN; - -const tableHeader = ['Name', 'Duration']; - -const collapsibleSection = (title: string, content: string) => `
\n${title}\n\n${content}\n
\n\n`; - -const buildDurationDetails = (title: string, entry: Stats, unit: string) => { - const relativeStdev = entry.stdev / entry.mean; - - return [ - `**${title}**`, - `Mean: ${format.formatMetric(entry.mean, unit)}`, - `Stdev: ${format.formatMetric(entry.stdev, unit)} (${format.formatPercent(relativeStdev)})`, - entry.entries ? `Runs: ${entry.entries.join(' ')}` : '', - ] - .filter(Boolean) - .join('
'); -}; - -const buildDurationDetailsEntry = (entry: Entry) => - ['baseline' in entry ? buildDurationDetails('Baseline', entry.baseline, entry.unit) : '', 'current' in entry ? buildDurationDetails('Current', entry.current, entry.unit) : ''] - .filter(Boolean) - .join('

'); - -const formatEntryDuration = (entry: Entry): string => { - if ('baseline' in entry && 'current' in entry) { - return format.formatMetricDiffChange(entry); - } - - if ('baseline' in entry) { - return format.formatMetric((entry as Entry).baseline.mean, (entry as Entry).unit); - } - - if ('current' in entry) { - return format.formatMetric((entry as Entry).current.mean, (entry as Entry).unit); - } - - return ''; -}; - -const buildDetailsTable = (entries: Entry[], numberOfTables = 1) => { - if (!entries.length) { - return ['']; - } - - // We always need at least one table - const safeNumberOfTables = numberOfTables === 0 ? 1 : numberOfTables; - - const entriesPerTable = Math.floor(entries.length / safeNumberOfTables); - const tables: string[] = []; - for (let i = 0; i < safeNumberOfTables; i++) { - const start = i * entriesPerTable; - const end = i === safeNumberOfTables - 1 ? entries.length : start + entriesPerTable; - const tableEntries = entries.slice(start, end); - - const rows = tableEntries.map((entry) => [entry.name, buildDurationDetailsEntry(entry)]); - const content = markdownTable([tableHeader, ...rows]); - - const tableMarkdown = collapsibleSection('Show details', content); - - tables.push(tableMarkdown); - } - - return tables; -}; - -const buildSummaryTable = (entries: Entry[], collapse = false) => { - if (!entries.length) { - return '_There are no entries_'; - } - - const rows = entries.map((entry) => [entry.name, formatEntryDuration(entry)]); - const content = markdownTable([tableHeader, ...rows]); - - return collapse ? collapsibleSection('Show entries', content) : content; -}; - -const buildMarkdown = (data: Data, skippedTests: string[], numberOfExtraFiles?: number): [string, ...string[]] => { - let singleFileOutput: string | undefined; - let nExtraFiles = numberOfExtraFiles ?? 0; - - // If the user didn't specify the number of extra files, calculate it based on the size of the single file - if (numberOfExtraFiles === undefined) { - singleFileOutput = buildMarkdown(data, skippedTests, 0)[0]; - const totalCharacters = singleFileOutput.length ?? 0; - - // If the single file is small enough, return it - if (totalCharacters <= MAX_CHARACTERS_PER_FILE_WITH_SAFETY_MARGIN) { - return [singleFileOutput]; - } - - // Otherwise, calculate the number of extra files needed - nExtraFiles = Math.ceil(totalCharacters / MAX_CHARACTERS_PER_FILE_WITH_SAFETY_MARGIN); - } - - let mainFile = '## Performance Comparison Report 📊'; - mainFile += nExtraFiles > 0 ? ` (1/${nExtraFiles + 1})` : ''; - - if (data.errors?.length) { - mainFile += '\n\n### Errors\n'; - for (const message of data.errors) { - mainFile += ` 1. 🛑 ${message}\n`; - } - } - - if (data.warnings?.length) { - mainFile += '\n\n### Warnings\n'; - for (const message of data.warnings) { - mainFile += ` 1. 🟡 ${message}\n`; - } - } - - if (skippedTests.length > 0) { - mainFile += `\n\n⚠️ Some tests did not pass successfully, so some results are omitted from final report: ${skippedTests.join(', ')}`; - } - - mainFile += '\n\n### Significant Changes To Duration'; - mainFile += `\n${buildSummaryTable(data.significance)}`; - mainFile += `\n${buildDetailsTable(data.significance, 1).at(0)}`; - - // We always need at least one table - const numberOfMeaninglessDetailsTables = nExtraFiles === 0 ? 1 : nExtraFiles; - const meaninglessDetailsTables = buildDetailsTable(data.meaningless, numberOfMeaninglessDetailsTables); - - if (nExtraFiles === 0) { - mainFile += '\n\n### Meaningless Changes To Duration'; - mainFile += `\n${buildSummaryTable(data.meaningless, true)}`; - mainFile += `\n${meaninglessDetailsTables.at(0)}`; - - return [mainFile]; - } - - const extraFiles: string[] = []; - for (let i = 0; i < nExtraFiles; i++) { - let extraFile = '## Performance Comparison Report 📊'; - extraFile += ` (${i + 2}/${nExtraFiles + 1})`; - - extraFile += '\n\n### Meaningless Changes To Duration'; - extraFile += nExtraFiles >= 2 ? ` (${i + 1}/${nExtraFiles})` : ''; - - extraFile += `\n${buildSummaryTable(data.meaningless, true)}`; - extraFile += `\n${meaninglessDetailsTables.at(i)}`; - extraFile += '\n'; - - extraFiles.push(extraFile); - } - - return [mainFile, ...extraFiles]; -}; - -const writeToFile = (filePath: string, content: string) => - fs - .writeFile(filePath, content) - .then(() => { - Logger.info(`✅ Written output markdown output file ${filePath}`); - Logger.info(`🔗 ${path.resolve(filePath)}\n`); - }) - .catch((error) => { - Logger.info(`❌ Could not write markdown output file ${filePath}`); - Logger.info(`🔗 ${path.resolve(filePath)}`); - console.error(error); - throw error; - }); - -const writeToMarkdown = (outputDir: string, data: Data, skippedTests: string[]) => { - const markdownFiles = buildMarkdown(data, skippedTests); - const filesString = markdownFiles.join('\n\n'); - Logger.info('Markdown was built successfully, writing to file...', filesString); - - if (markdownFiles.length === 1) { - return writeToFile(path.join(outputDir, 'output1.md'), markdownFiles[0]); - } - - return Promise.all( - markdownFiles.map((file, index) => { - const filePath = `${outputDir}/output${index + 1}.md`; - return writeToFile(filePath, file).catch((error) => { - console.error(error); - throw error; - }); - }), - ); -}; - -export default writeToMarkdown; -export {buildMarkdown}; diff --git a/tests/e2e/compare/output/markdownTable.ts b/tests/e2e/compare/output/markdownTable.ts deleted file mode 100644 index 5fe976159202..000000000000 --- a/tests/e2e/compare/output/markdownTable.ts +++ /dev/null @@ -1,356 +0,0 @@ -// copied from https://raw.githubusercontent.com/wooorm/markdown-table/main/index.js, turned into cmjs - -type MarkdownTableOptions = { - /** - * One style for all columns, or styles for their respective columns. - * Each style is either `'l'` (left), `'r'` (right), or `'c'` (center). - * Other values are treated as `''`, which doesn’t place the colon in the - * alignment row but does align left. - * *Only the lowercased first character is used, so `Right` is fine.* - */ - align?: string | null | Array; - - /** - * Whether to add a space of padding between delimiters and cells. - * - * When `true`, there is padding: - * - * ```markdown - * | Alpha | B | - * | ----- | ----- | - * | C | Delta | - * ``` - * - * When `false`, there is no padding: - * - * ```markdown - * |Alpha|B | - * |-----|-----| - * |C |Delta| - * ``` - */ - padding?: boolean; - - /** - * Whether to begin each row with the delimiter. - * - * > 👉 **Note**: please don’t use this: it could create fragile structures - * > that aren’t understandable to some markdown parsers. - * - * When `true`, there are starting delimiters: - * - * ```markdown - * | Alpha | B | - * | ----- | ----- | - * | C | Delta | - * ``` - * - * When `false`, there are no starting delimiters: - * - * ```markdown - * Alpha | B | - * ----- | ----- | - * C | Delta | - * ``` - */ - delimiterStart?: boolean; - /** - * Whether to end each row with the delimiter. - * - * > 👉 **Note**: please don’t use this: it could create fragile structures - * > that aren’t understandable to some markdown parsers. - * - * When `true`, there are ending delimiters: - * - * ```markdown - * | Alpha | B | - * | ----- | ----- | - * | C | Delta | - * ``` - * - * When `false`, there are no ending delimiters: - * - * ```markdown - * | Alpha | B - * | ----- | ----- - * | C | Delta - * ``` - */ - delimiterEnd?: boolean; - - /** - * Whether to align the delimiters. - * By default, they are aligned: - * - * ```markdown - * | Alpha | B | - * | ----- | ----- | - * | C | Delta | - * ``` - * - * Pass `false` to make them staggered: - * - * ```markdown - * | Alpha | B | - * | - | - | - * | C | Delta | - * ``` - */ - alignDelimiters?: boolean; - - /** - * Function to detect the length of table cell content. - * This is used when aligning the delimiters (`|`) between table cells. - * Full-width characters and emoji mess up delimiter alignment when viewing - * the markdown source. - * To fix this, you can pass this function, which receives the cell content - * and returns its “visible” size. - * Note that what is and isn’t visible depends on where the text is displayed. - * - * Without such a function, the following: - * - * ```js - * markdownTable([ - * ['Alpha', 'Bravo'], - * ['中文', 'Charlie'], - * ['👩‍❤️‍👩', 'Delta'] - * ]) - * ``` - * - * Yields: - * - * ```markdown - * | Alpha | Bravo | - * | - | - | - * | 中文 | Charlie | - * | 👩‍❤️‍👩 | Delta | - * ``` - * - * With [`string-width`](https://github.com/sindresorhus/string-width): - * - * ```js - * import stringWidth from 'string-width' - * - * markdownTable( - * [ - * ['Alpha', 'Bravo'], - * ['中文', 'Charlie'], - * ['👩‍❤️‍👩', 'Delta'] - * ], - * {stringLength: stringWidth} - * ) - * ``` - * - * Yields: - * - * ```markdown - * | Alpha | Bravo | - * | ----- | ------- | - * | 中文 | Charlie | - * | 👩‍❤️‍👩 | Delta | - * ``` - */ - stringLength?: (value: string) => number; -}; - -function serialize(value: string | null | undefined): string { - return value === null || value === undefined ? '' : String(value); -} - -function defaultStringLength(value: string): number { - return value.length; -} - -function toAlignment(value: string | null | undefined): number { - const code = typeof value === 'string' ? value.codePointAt(0) : 0; - - if (code === 67 /* `C` */ || code === 99 /* `c` */) { - return 99; /* `c` */ - } - - if (code === 76 /* `L` */ || code === 108 /* `l` */) { - return 108; /* `l` */ - } - - if (code === 82 /* `R` */ || code === 114 /* `r` */) { - return 114; /* `r` */ - } - - return 0; -} - -/** Generate a markdown ([GFM](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables)) table.. */ -function markdownTable(table: Array>, options: MarkdownTableOptions = {}) { - const align = (options.align ?? []).concat(); - const stringLength = options.stringLength ?? defaultStringLength; - /** Character codes as symbols for alignment per column. */ - const alignments: number[] = []; - /** Cells per row. */ - const cellMatrix: string[][] = []; - /** Sizes of each cell per row. */ - const sizeMatrix: number[][] = []; - const longestCellByColumn: number[] = []; - let mostCellsPerRow = 0; - let rowIndex = -1; - - // This is a superfluous loop if we don’t align delimiters, but otherwise we’d - // do superfluous work when aligning, so optimize for aligning. - while (++rowIndex < table.length) { - const row: string[] = []; - const sizes: number[] = []; - let columnIndex = -1; - - const rowData = table.at(rowIndex) ?? []; - - if (rowData.length > mostCellsPerRow) { - mostCellsPerRow = rowData.length; - } - - while (++columnIndex < rowData.length) { - const cell = serialize(rowData.at(columnIndex)); - - if (options.alignDelimiters !== false) { - const size = stringLength(cell); - sizes[columnIndex] = size; - - if (longestCellByColumn.at(columnIndex) === undefined || size > (longestCellByColumn.at(columnIndex) ?? 0)) { - longestCellByColumn[columnIndex] = size; - } - } - - row.push(cell); - } - - cellMatrix[rowIndex] = row; - sizeMatrix[rowIndex] = sizes; - } - - // Figure out which alignments to use. - let columnIndex = -1; - - if (typeof align === 'object' && 'length' in align) { - while (++columnIndex < mostCellsPerRow) { - alignments[columnIndex] = toAlignment(align.at(columnIndex)); - } - } else { - const code = toAlignment(align); - - while (++columnIndex < mostCellsPerRow) { - alignments[columnIndex] = code; - } - } - - // Inject the alignment row. - columnIndex = -1; - const row: string[] = []; - const sizes: number[] = []; - - while (++columnIndex < mostCellsPerRow) { - const code = alignments.at(columnIndex); - let before = ''; - let after = ''; - - if (code === 99 /* `c` */) { - before = ':'; - after = ':'; - } else if (code === 108 /* `l` */) { - before = ':'; - } else if (code === 114 /* `r` */) { - after = ':'; - } - - // There *must* be at least one hyphen-minus in each alignment cell. - let size = options.alignDelimiters === false ? 1 : Math.max(1, (longestCellByColumn.at(columnIndex) ?? 0) - before.length - after.length); - - const cell = before + '-'.repeat(size) + after; - - if (options.alignDelimiters !== false) { - size = before.length + size + after.length; - - if (size > (longestCellByColumn.at(columnIndex) ?? 0)) { - longestCellByColumn[columnIndex] = size; - } - - sizes[columnIndex] = size; - } - - row[columnIndex] = cell; - } - - // Inject the alignment row. - cellMatrix.splice(1, 0, row); - sizeMatrix.splice(1, 0, sizes); - - rowIndex = -1; - const lines: string[] = []; - - while (++rowIndex < cellMatrix.length) { - const matrixRow = cellMatrix.at(rowIndex); - const matrixSizes = sizeMatrix.at(rowIndex); - columnIndex = -1; - const line: string[] = []; - - while (++columnIndex < mostCellsPerRow) { - const cell = matrixRow?.at(columnIndex) ?? ''; - let before = ''; - let after = ''; - - if (options.alignDelimiters !== false) { - const size = (longestCellByColumn.at(columnIndex) ?? 0) - (matrixSizes?.at(columnIndex) ?? 0); - const code = alignments.at(columnIndex); - - if (code === 114 /* `r` */) { - before = ' '.repeat(size); - } else if (code === 99 /* `c` */) { - if (size % 2) { - before = ' '.repeat(size / 2 + 0.5); - after = ' '.repeat(size / 2 - 0.5); - } else { - before = ' '.repeat(size / 2); - after = before; - } - } else { - after = ' '.repeat(size); - } - } - - if (options.delimiterStart !== false && !columnIndex) { - line.push('|'); - } - - if ( - options.padding !== false && - // Don’t add the opening space if we’re not aligning and the cell is - // empty: there will be a closing space. - !(options.alignDelimiters === false && cell === '') && - (options.delimiterStart !== false || columnIndex) - ) { - line.push(' '); - } - - if (options.alignDelimiters !== false) { - line.push(before); - } - - line.push(cell); - - if (options.alignDelimiters !== false) { - line.push(after); - } - - if (options.padding !== false) { - line.push(' '); - } - - if (options.delimiterEnd !== false || columnIndex !== mostCellsPerRow - 1) { - line.push('|'); - } - } - - lines.push(options.delimiterEnd === false ? line.join('').replaceAll(/ +$/g, '') : line.join('')); - } - - return lines.join('\n'); -} - -export default markdownTable; diff --git a/tests/e2e/config.dev.ts b/tests/e2e/config.dev.ts deleted file mode 100644 index 6ee212ad30f8..000000000000 --- a/tests/e2e/config.dev.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {Config} from './config.local'; - -const packageName = 'com.expensify.chat.dev'; -const appPath = './android/app/build/intermediates/apk/development/debug/app-development-debug.apk'; - -const config: Config = { - MAIN_APP_PACKAGE: packageName, - DELTA_APP_PACKAGE: packageName, - BRANCH_MAIN: 'main', - BRANCH_DELTA: 'main', - MAIN_APP_PATH: appPath, - DELTA_APP_PATH: appPath, - RUNS: 8, - BOOT_COOL_DOWN: 5 * 1000, - FLAG: '-t', -}; - -export default config; diff --git a/tests/e2e/config.local.ts b/tests/e2e/config.local.ts deleted file mode 100644 index 8e90da9d3423..000000000000 --- a/tests/e2e/config.local.ts +++ /dev/null @@ -1,13 +0,0 @@ -type Config = Record; - -const config: Config = { - MAIN_APP_PACKAGE: 'com.expensify.chat.e2e', - DELTA_APP_PACKAGE: 'com.expensify.chat.e2edelta', - MAIN_APP_PATH: './android/app/build/outputs/apk/e2e/release/app-e2e-release.apk', - DELTA_APP_PATH: './android/app/build/outputs/apk/e2edelta/release/app-e2edelta-release.apk', - BOOT_COOL_DOWN: 1 * 1000, - RUNS: 8, -}; - -export default config; -export type {Config}; diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts deleted file mode 100644 index 2a3ea48581c0..000000000000 --- a/tests/e2e/config.ts +++ /dev/null @@ -1,111 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- if the first value is '' nullish coalescing will return '' so leaving || for safety -const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results'; - -// add your test name here … -const TEST_NAMES = { - AppStartTime: 'App start time', - OpenSearchRouter: 'Open search router TTI', - ReportTyping: 'Report typing', - ChatOpening: 'Chat opening', - Linking: 'Linking', - MoneyRequest: 'Money request', -}; - -/** - * Default config, used by CI by default. - * You can modify these values for your test run by creating a - * separate config file and pass it to the test runner like this: - * - * ```bash - * npm run test:e2e -- --config ./path/to/your/config.js - * ``` - */ -export default { - MAIN_APP_PACKAGE: 'com.expensify.chat.e2e', - DELTA_APP_PACKAGE: 'com.expensify.chat.e2edelta', - - MAIN_APP_PATH: './app-e2eRelease.apk', - DELTA_APP_PATH: './app-e2edeltaRelease.apk', - FLAG: '', - - BRANCH_MAIN: 'main', - BRANCH_DELTA: 'delta', - - ENTRY_FILE: 'src/libs/E2E/reactNativeLaunchingTest.ts', - - // The path to the activity within the app that we want to launch. - // Note: even though we have different package _names_, this path doesn't change. - ACTIVITY_PATH: 'com.expensify.chat.MainActivity', - - // The port of the testing server that communicates with the app - SERVER_PORT: 4723, - - // The amount of times a test should be executed for average performance metrics - RUNS: 60, - - DEFAULT_BASELINE_BRANCH: 'main', - - OUTPUT_DIR, - - // The file to write intermediate results to - OUTPUT_FILE_CURRENT: `${OUTPUT_DIR}/current.json`, - - // The file we write logs to - LOG_FILE: `${OUTPUT_DIR}/debug.log`, - - // The time in milliseconds after which an operation fails due to timeout - INTERACTION_TIMEOUT: 150 * 1000, - - // Period we wait between each test runs, to let the device cool down - BOOT_COOL_DOWN: 90 * 1000, - - // Period we wait between each test runs, to let the device cool down - SUITE_COOL_DOWN: 10 * 1000, - - TEST_NAMES, - - /** - * Add your test configurations here. At least, - * you need to add a name for your test. - * - * @type {Object.} - */ - TESTS_CONFIG: { - [TEST_NAMES.AppStartTime]: { - name: TEST_NAMES.AppStartTime, - // ... any additional config you might need - }, - [TEST_NAMES.OpenSearchRouter]: { - name: TEST_NAMES.OpenSearchRouter, - }, - [TEST_NAMES.ReportTyping]: { - name: TEST_NAMES.ReportTyping, - reportScreen: { - autoFocus: true, - }, - // Crowded Policy (Do Not Delete) Report, has a input bar available: - reportID: '8268282951170052', - message: `Measure_performance#${Math.floor(Math.random() * 1000000)}`, - }, - [TEST_NAMES.ChatOpening]: { - name: TEST_NAMES.ChatOpening, - // #announce Chat with many messages - reportID: '5421294415618529', - }, - [TEST_NAMES.Linking]: { - name: TEST_NAMES.Linking, - reportScreen: { - autoFocus: true, - }, - // Crowded Policy (Do Not Delete) Report, has a input bar available: - reportID: '8268282951170052', - linkedReportID: '5421294415618529', - linkedReportActionID: '2845024374735019929', - }, - [TEST_NAMES.MoneyRequest]: { - name: TEST_NAMES.MoneyRequest, - }, - }, -}; - -export {TEST_NAMES}; diff --git a/tests/e2e/measure/math.ts b/tests/e2e/measure/math.ts deleted file mode 100644 index 4c4ef1c9ff1e..000000000000 --- a/tests/e2e/measure/math.ts +++ /dev/null @@ -1,52 +0,0 @@ -type Entries = number[]; - -type Stats = { - mean: number; - stdev: number; - runs: number; - entries: Entries; -}; - -const filterOutliersViaIQR = (data: Entries): Entries => { - let q1; - let q3; - - const values = data.slice().sort((a, b) => a - b); - - if ((values.length / 4) % 1 === 0) { - q1 = (1 / 2) * ((values.at(values.length / 4) ?? 0) + (values.at(values.length / 4 + 1) ?? 0)); - q3 = (1 / 2) * ((values.at(values.length * (3 / 4)) ?? 0) + (values.at(values.length * (3 / 4) + 1) ?? 0)); - } else { - q1 = values.at(Math.floor(values.length / 4 + 1)) ?? 0; - q3 = values.at(Math.ceil(values.length * (3 / 4) + 1)) ?? 0; - } - - const iqr = q3 - q1; - const maxValue = q3 + iqr * 1.5; - const minValue = q1 - iqr * 1.5; - - return values.filter((x) => x >= minValue && x <= maxValue); -}; - -const mean = (arr: Entries): number => arr.reduce((a, b) => a + b, 0) / arr.length; - -const std = (arr: Entries): number => { - const avg = mean(arr); - return Math.sqrt(arr.map((i) => (i - avg) ** 2).reduce((a, b) => a + b) / arr.length); -}; - -const getStats = (entries: Entries): Stats => { - const cleanedEntries = filterOutliersViaIQR(entries); - const meanDuration = mean(cleanedEntries); - const stdevDuration = std(cleanedEntries); - - return { - mean: meanDuration, - stdev: stdevDuration, - runs: cleanedEntries.length, - entries: cleanedEntries, - }; -}; - -export default getStats; -export type {Stats}; diff --git a/tests/e2e/nativeCommands/NativeCommandsAction.ts b/tests/e2e/nativeCommands/NativeCommandsAction.ts deleted file mode 100644 index c26582161af9..000000000000 --- a/tests/e2e/nativeCommands/NativeCommandsAction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {NativeCommand} from '@libs/E2E/client'; - -const NativeCommandsAction = { - scroll: 'scroll', - type: 'type', - backspace: 'backspace', - clear: 'clear', -} as const; - -const makeTypeTextCommand = (text: string): NativeCommand => ({ - actionName: NativeCommandsAction.type, - payload: { - text, - }, -}); - -const makeBackspaceCommand = (): NativeCommand => ({ - actionName: NativeCommandsAction.backspace, -}); - -const makeClearCommand = (): NativeCommand => ({ - actionName: NativeCommandsAction.clear, -}); - -export {NativeCommandsAction, makeTypeTextCommand, makeBackspaceCommand, makeClearCommand}; diff --git a/tests/e2e/nativeCommands/adbBackspace.ts b/tests/e2e/nativeCommands/adbBackspace.ts deleted file mode 100644 index 112a80bdb37e..000000000000 --- a/tests/e2e/nativeCommands/adbBackspace.ts +++ /dev/null @@ -1,9 +0,0 @@ -import execAsync from '../utils/execAsync'; -import * as Logger from '../utils/logger'; - -const adbBackspace = (): Promise => { - Logger.log(`🔙 Pressing backspace`); - return execAsync(`adb shell input keyevent KEYCODE_DEL`).then(() => true); -}; - -export default adbBackspace; diff --git a/tests/e2e/nativeCommands/adbClear.ts b/tests/e2e/nativeCommands/adbClear.ts deleted file mode 100644 index 5e25739b73a7..000000000000 --- a/tests/e2e/nativeCommands/adbClear.ts +++ /dev/null @@ -1,17 +0,0 @@ -import execAsync from '../utils/execAsync'; -import * as Logger from '../utils/logger'; - -const adbClear = (): Promise => { - Logger.log(`🧹 Clearing the typed text`); - return execAsync(` - function clear_input() { - adb shell input keyevent KEYCODE_MOVE_END - # delete up to 2 characters per 1 press, so 1..3 will delete up to 6 characters - adb shell input keyevent --longpress $(printf 'KEYCODE_DEL %.0s' {1..3}) - } - - clear_input - `).then(() => true); -}; - -export default adbClear; diff --git a/tests/e2e/nativeCommands/adbTypeText.ts b/tests/e2e/nativeCommands/adbTypeText.ts deleted file mode 100644 index deea16b198c8..000000000000 --- a/tests/e2e/nativeCommands/adbTypeText.ts +++ /dev/null @@ -1,9 +0,0 @@ -import execAsync from '../utils/execAsync'; -import * as Logger from '../utils/logger'; - -const adbTypeText = (text: string) => { - Logger.log(`📝 Typing text: ${text}`); - return execAsync(`adb shell input text "${text}"`).then(() => true); -}; - -export default adbTypeText; diff --git a/tests/e2e/nativeCommands/index.ts b/tests/e2e/nativeCommands/index.ts deleted file mode 100644 index 6331bae463ba..000000000000 --- a/tests/e2e/nativeCommands/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type {NativeCommandPayload} from '@libs/E2E/client'; -import adbBackspace from './adbBackspace'; -import adbClear from './adbClear'; -import adbTypeText from './adbTypeText'; -// eslint-disable-next-line rulesdir/prefer-import-module-contents -import {NativeCommandsAction} from './NativeCommandsAction'; - -const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload): Promise => { - switch (actionName) { - case NativeCommandsAction.scroll: - throw new Error('Not implemented yet'); - case NativeCommandsAction.type: - return adbTypeText(payload?.text ?? ''); - case NativeCommandsAction.backspace: - return adbBackspace(); - case NativeCommandsAction.clear: - return adbClear(); - default: - throw new Error(`Unknown action: ${actionName}`); - } -}; - -export {NativeCommandsAction, executeFromPayload, adbTypeText}; diff --git a/tests/e2e/server/index.ts b/tests/e2e/server/index.ts deleted file mode 100644 index ab11e9229b7b..000000000000 --- a/tests/e2e/server/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -import type {IncomingMessage, ServerResponse} from 'http'; -import {createServer} from 'http'; -import type {NativeCommand, TestResult} from '@libs/E2E/client'; -import type {NetworkCacheMap, TestConfig} from '@libs/E2E/types'; -import config from '../config'; -import * as nativeCommands from '../nativeCommands'; -import * as Logger from '../utils/logger'; -import Routes from './routes'; - -type NetworkCache = { - appInstanceId: string; - cache: NetworkCacheMap; -}; - -type RequestData = TestResult | NativeCommand | NetworkCache; - -type TestStartedListener = (testConfig?: TestConfig) => void; - -type TestDoneListener = () => void; - -type TestResultListener = (testResult: TestResult) => void; - -type AddListener = (listener: TListener) => () => void; - -type ClearAllListeners = () => void; - -type ServerInstance = { - setTestConfig: (testConfig: TestConfig) => void; - getTestConfig: () => TestConfig; - addTestStartedListener: AddListener; - addTestResultListener: AddListener; - addTestDoneListener: AddListener; - clearAllTestDoneListeners: ClearAllListeners; - forceTestCompletion: () => void; - setReadyToAcceptTestResults: (isReady: boolean) => void; - isReadyToAcceptTestResults: boolean; - start: () => Promise; - stop: () => Promise; -}; - -const PORT = process.env.PORT ?? config.SERVER_PORT; - -// Gets the request data as a string -const getReqData = (req: IncomingMessage): Promise => { - let data = ''; - req.on('data', (chunk: string) => { - data += chunk; - }); - - return new Promise((resolve) => { - req.on('end', () => { - resolve(data); - }); - }); -}; - -// Expects a POST request with JSON data. Returns parsed JSON data. -const getPostJSONRequestData = (req: IncomingMessage, res: ServerResponse): Promise | undefined => { - if (req.method !== 'POST') { - res.statusCode = 400; - res.end('Unsupported method'); - return; - } - - return getReqData(req).then((data): TRequestData | undefined => { - try { - return JSON.parse(data) as TRequestData; - } catch (e) { - Logger.info('❌ Failed to parse request data', data); - res.statusCode = 400; - res.end('Invalid JSON'); - } - }); -}; - -const createListenerState = (): [TListener[], AddListener, ClearAllListeners] => { - const listeners: TListener[] = []; - const addListener = (listener: TListener) => { - listeners.push(listener); - return () => { - const index = listeners.indexOf(listener); - if (index !== -1) { - listeners.splice(index, 1); - } - }; - }; - const clearAllListeners = () => { - listeners.splice(0, listeners.length); - }; - - return [listeners, addListener, clearAllListeners]; -}; - -/** - * Creates a new http server. - * The server just has two endpoints: - * - * - POST: /test_results, expects a {@link TestResult} as JSON body. - * Send test results while a test runs. - * - GET: /test_done, expected to be called when test run signals it's done - * - * It returns an instance to which you can add listeners for the test results, and test done events. - */ -const createServerInstance = (): ServerInstance => { - const [testStartedListeners, addTestStartedListener] = createListenerState(); - const [testResultListeners, addTestResultListener] = createListenerState(); - const [testDoneListeners, addTestDoneListener, clearAllTestDoneListeners] = createListenerState(); - let isReadyToAcceptTestResults = true; - - const setReadyToAcceptTestResults = (isReady: boolean) => { - isReadyToAcceptTestResults = isReady; - }; - - const forceTestCompletion = () => { - for (const listener of testDoneListeners) { - listener(); - } - }; - - let activeTestConfig: TestConfig | undefined; - const networkCache: Record = {}; - - const setTestConfig = (testConfig: TestConfig) => { - activeTestConfig = testConfig; - }; - const getTestConfig = (): TestConfig => { - if (!activeTestConfig) { - throw new Error('No test config set'); - } - - return activeTestConfig; - }; - - const server = createServer((req, res): ServerResponse | void => { - res.statusCode = 200; - switch (req.url) { - case Routes.testConfig: { - for (const listener of testStartedListeners) { - listener(activeTestConfig); - } - if (!activeTestConfig) { - throw new Error('No test config set'); - } - return res.end(JSON.stringify(activeTestConfig)); - } - - case Routes.testResults: { - if (!isReadyToAcceptTestResults) { - return res.end('ok'); - } - - getPostJSONRequestData(req, res)?.then((data) => { - if (!data) { - // The getPostJSONRequestData function already handled the response - return; - } - - for (const listener of testResultListeners) { - listener(data); - } - - res.end('ok'); - }); - break; - } - - case Routes.testDone: { - forceTestCompletion(); - return res.end('ok'); - } - - case Routes.testNativeCommand: { - getPostJSONRequestData(req, res) - ?.then((data) => nativeCommands.executeFromPayload(data?.actionName, data?.payload)) - .then((status) => { - if (status) { - res.end('ok'); - return; - } - res.statusCode = 500; - res.end('Error executing command'); - }) - .catch((error: string) => { - Logger.error('Error executing command', error); - res.statusCode = 500; - res.end('Error executing command'); - }); - break; - } - - case Routes.testGetNetworkCache: { - getPostJSONRequestData(req, res)?.then((data) => { - const appInstanceId = data?.appInstanceId; - if (!appInstanceId) { - res.statusCode = 400; - res.end('Invalid request missing appInstanceId'); - return; - } - - const cachedData = networkCache[appInstanceId] ?? {}; - res.end(JSON.stringify(cachedData)); - }); - - break; - } - - case Routes.testUpdateNetworkCache: { - getPostJSONRequestData(req, res)?.then((data) => { - const appInstanceId = data?.appInstanceId; - const cache = data?.cache; - if (!appInstanceId || !cache) { - res.statusCode = 400; - res.end('Invalid request missing appInstanceId or cache'); - return; - } - - networkCache[appInstanceId] = cache; - res.end('ok'); - }); - - break; - } - - default: - res.statusCode = 404; - res.end('Page not found!'); - } - }); - - return { - setReadyToAcceptTestResults, - get isReadyToAcceptTestResults() { - return isReadyToAcceptTestResults; - }, - setTestConfig, - getTestConfig, - addTestStartedListener, - addTestResultListener, - addTestDoneListener, - clearAllTestDoneListeners, - forceTestCompletion, - start: () => - new Promise((resolve) => { - server.listen(PORT, resolve); - }), - stop: () => - new Promise((resolve) => { - server.close(resolve); - }), - }; -}; - -export default createServerInstance; diff --git a/tests/e2e/server/routes.ts b/tests/e2e/server/routes.ts deleted file mode 100644 index 0a204f491d18..000000000000 --- a/tests/e2e/server/routes.ts +++ /dev/null @@ -1,21 +0,0 @@ -const routes = { - // The app calls this endpoint to know which test to run - testConfig: '/test_config', - - // When running a test the app reports the results to this endpoint - testResults: '/test_results', - - // When the app is done running a test it calls this endpoint - testDone: '/test_done', - - // Commands to execute from the host machine (there are pre-defined types like scroll or type) - testNativeCommand: '/test_native_command', - - // Updates the network cache - testUpdateNetworkCache: '/test_update_network_cache', - - // Gets the network cache - testGetNetworkCache: '/test_get_network_cache', -} satisfies Record; - -export default routes; diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts deleted file mode 100644 index 3cb52a6589ea..000000000000 --- a/tests/e2e/testRunner.ts +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Multifaceted script, its main function is running the e2e tests. - * - * When running in a local environment it can take care of building the APKs required for e2e testing - * When running on the CI (depending on the flags passed to it) it will skip building and just package/re-sign - * the correct e2e JS bundle into an existing APK - * - * It will run only one set of tests per branch, for you to compare results to get a performance analysis - * You need to run it twice, once with the base branch (--branch main) and another time with another branch - * and a label to (--branch my_branch --label delta) - * - * This two runs will generate a main.json and a delta.json with the performance data, which then you can merge via - * node tests/e2e/merge.js - */ -/* eslint-disable no-restricted-syntax,no-await-in-loop */ -import {execSync} from 'child_process'; -import fs from 'fs'; -import type {TestResult} from '@libs/E2E/client'; -import type {TestConfig, Unit} from '@libs/E2E/types'; -import compare from './compare/compare'; -import defaultConfig from './config'; -import createServerInstance from './server'; -import reversePort from './utils/androidReversePort'; -import closeANRPopup from './utils/closeANRPopup'; -import installApp from './utils/installApp'; -import killApp from './utils/killApp'; -import launchApp from './utils/launchApp'; -import * as Logger from './utils/logger'; -import * as MeasureUtils from './utils/measure'; -import sleep from './utils/sleep'; -import withFailTimeout from './utils/withFailTimeout'; - -type Result = Record; - -type CustomConfig = { - default: unknown; -}; - -// VARIABLE CONFIGURATION -const args = process.argv.slice(2); -const getArg = (argName: string): string | undefined => { - const argIndex = args.indexOf(argName); - if (argIndex === -1) { - return undefined; - } - return args.at(argIndex + 1); -}; - -let config = defaultConfig; -const setConfigPath = (configPathParam: string | undefined) => { - let configPath = configPathParam; - if (!configPath?.startsWith('.')) { - configPath = `./${configPath}`; - } - const customConfig = (require(configPath) as CustomConfig).default; - config = Object.assign(defaultConfig, customConfig); -}; - -if (args.includes('--config')) { - const configPath = getArg('--config'); - setConfigPath(configPath); -} - -// Important: set app path only after correct config file has been loaded -const mainAppPath = getArg('--mainAppPath') ?? config.MAIN_APP_PATH; -const deltaAppPath = getArg('--deltaAppPath') ?? config.DELTA_APP_PATH; -// Check if files exists: -if (!fs.existsSync(mainAppPath)) { - throw new Error(`Main app path does not exist: ${mainAppPath}`); -} -if (!fs.existsSync(deltaAppPath)) { - throw new Error(`Delta app path does not exist: ${deltaAppPath}`); -} - -// On CI it is important to re-create the output dir, it has a different owner -// therefore this process cannot write to it -try { - fs.rmSync(config.OUTPUT_DIR, {recursive: true, force: true}); - - fs.mkdirSync(config.OUTPUT_DIR); -} catch (error) { - // Do nothing - console.error(error); -} - -// START OF TEST CODE -const runTests = async (): Promise => { - Logger.info('Installing apps and reversing port'); - await installApp(config.MAIN_APP_PACKAGE, mainAppPath, undefined, defaultConfig.FLAG); - await installApp(config.DELTA_APP_PACKAGE, deltaAppPath, undefined, defaultConfig.FLAG); - await reversePort(); - - // Start the HTTP server - const server = createServerInstance(); - await server.start(); - - // Create a dict in which we will store the run durations for all tests - const results: Record = {}; - const metricForTest: Record = {}; - - const attachTestResult = (testResult: TestResult) => { - let result = 0; - - if (testResult?.metric !== undefined) { - if (testResult.metric < 0) { - return; - } - result = testResult.metric; - } - - Logger.log(`[LISTENER] Test '${testResult?.name}' on '${testResult?.branch}' measured ${result}${testResult.unit}`); - - if (testResult?.branch && !results[testResult.branch]) { - results[testResult.branch] = {}; - } - - if (testResult?.branch && testResult?.name) { - results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] ?? []).concat(result); - } - - if (!metricForTest[testResult.name] && testResult.unit) { - metricForTest[testResult.name] = testResult.unit; - } - }; - - const skippedTests: string[] = []; - const clearTestResults = (test: TestConfig) => { - skippedTests.push(test.name); - for (const branch of Object.keys(results)) { - for (const metric of Object.keys(results[branch])) { - if (!metric.startsWith(test.name)) { - continue; - } - delete results[branch][metric]; - } - } - }; - - // Collect results while tests are being executed - server.addTestResultListener((testResult) => { - const {isCritical = true} = testResult; - - if (testResult?.error != null && isCritical) { - throw new Error(`Test '${testResult.name}' failed with error: ${testResult.error}`); - } - if (testResult?.error != null && !isCritical) { - // force test completion, since we don't want to have timeout error for non being execute test - server.forceTestCompletion(); - Logger.warn(`Test '${testResult.name}' failed with error: ${testResult.error}`); - } - - attachTestResult(testResult); - }); - - // Function to run a single test iteration - async function runTestIteration(appPackage: string, iterationText: string, branch: string, launchArgs: Record = {}): Promise { - Logger.info(iterationText); - - // Making sure the app is really killed (e.g. if a prior test run crashed) - Logger.log('Killing', appPackage); - await killApp('android', appPackage); - - Logger.log('Launching', appPackage); - await launchApp('android', appPackage, config.ACTIVITY_PATH, launchArgs); - - const {promise, resetTimeout} = withFailTimeout( - new Promise((resolve, reject) => { - const removeListener = server.addTestDoneListener(() => { - Logger.success(iterationText); - - const metrics = MeasureUtils.stop('done'); - const test = server.getTestConfig(); - - if (server.isReadyToAcceptTestResults) { - attachTestResult({ - name: `${test.name} (CPU)`, - branch, - metric: metrics.cpu, - unit: '%', - }); - attachTestResult({ - name: `${test.name} (FPS)`, - branch, - metric: metrics.fps, - unit: 'FPS', - }); - attachTestResult({ - name: `${test.name} (RAM)`, - branch, - metric: metrics.ram, - unit: 'MB', - }); - attachTestResult({ - name: `${test.name} (CPU/JS)`, - branch, - metric: metrics.jsThread, - unit: '%', - }); - attachTestResult({ - name: `${test.name} (CPU/UI)`, - branch, - metric: metrics.uiThread, - unit: '%', - }); - } - removeListener(); - resolve(); - }); - MeasureUtils.start(appPackage, { - onAttachFailed: async () => { - Logger.warn('The PID has changed, trying to restart the test...'); - MeasureUtils.stop('retry'); - resetTimeout(); - removeListener(); - // something went wrong, let's wait a little bit and try again - await sleep(5000); - try { - // simply restart the test - await runTestIteration(appPackage, iterationText, branch, launchArgs); - resolve(); - } catch (e) { - // okay, give up and throw the exception further - reject(e); - } - }, - }); - }), - iterationText, - ); - await promise; - - Logger.log('Killing', appPackage); - await killApp('android', appPackage); - } - - // Run the tests - const tests = Object.keys(config.TESTS_CONFIG); - for (let testIndex = 0; testIndex < tests.length; testIndex++) { - const test = Object.values(config.TESTS_CONFIG).at(testIndex); - - // re-instal app for each new test suite - await installApp(config.MAIN_APP_PACKAGE, mainAppPath, undefined, defaultConfig.FLAG); - await installApp(config.DELTA_APP_PACKAGE, deltaAppPath, undefined, defaultConfig.FLAG); - - // check if we want to skip the test - if (args.includes('--includes')) { - const includes = args[args.indexOf('--includes') + 1]; - - // assume that "includes" is a regexp - if (!test?.name?.match(includes)) { - continue; - } - } - - // Having the cool-down right at the beginning lowers the chances of heat - // throttling from the previous run (which we have no control over and will be a - // completely different AWS DF customer/app). It also gives the time to cool down between tests. - Logger.info(`Cooling down for ${config.BOOT_COOL_DOWN / 1000}s`); - await sleep(config.BOOT_COOL_DOWN); - - server.setTestConfig(test as TestConfig); - server.setReadyToAcceptTestResults(false); - - try { - const warmupText = `Warmup for test '${test?.name}' [${testIndex + 1}/${tests.length}]`; - - // For each warmup we allow the warmup to fail three times before we stop the warmup run: - const errorCountWarmupRef = { - errorCount: 0, - allowedExceptions: 3, - }; - - // by default we do 2 warmups: - // - first warmup to pass a login flow - // - second warmup to pass an actual flow and cache network requests - const iterations = 2; - for (let i = 0; i < iterations; i++) { - try { - // Warmup the main app: - await runTestIteration(config.MAIN_APP_PACKAGE, `[MAIN] ${warmupText}. Iteration ${i + 1}/${iterations}`, config.BRANCH_MAIN); - - // Warmup the delta app: - await runTestIteration(config.DELTA_APP_PACKAGE, `[DELTA] ${warmupText}. Iteration ${i + 1}/${iterations}`, config.BRANCH_DELTA); - } catch (e) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Logger.error(`Warmup failed with error: ${e}`); - - await closeANRPopup(); - MeasureUtils.stop('error-warmup'); - server.clearAllTestDoneListeners(); - - errorCountWarmupRef.errorCount++; - i--; // repeat warmup again - - if (errorCountWarmupRef.errorCount === errorCountWarmupRef.allowedExceptions) { - Logger.error("There was an error running the warmup and we've reached the maximum number of allowed exceptions. Stopping the test run."); - throw e; - } - } - } - - server.setReadyToAcceptTestResults(true); - - // For each test case we allow the test to fail three times before we stop the test run: - const errorCountRef = { - errorCount: 0, - allowedExceptions: 3, - }; - - // We run each test multiple time to average out the results - for (let testIteration = 0; testIteration < config.RUNS; testIteration++) { - const onError = async (e: Error) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Logger.error(`Unexpected error during test execution: ${e}. `); - MeasureUtils.stop('error'); - await closeANRPopup(); - server.clearAllTestDoneListeners(); - errorCountRef.errorCount += 1; - if (testIteration === 0 || errorCountRef.errorCount === errorCountRef.allowedExceptions) { - Logger.error("There was an error running the test and we've reached the maximum number of allowed exceptions. Stopping the test run."); - // If the error happened on the first test run, the test is broken - // and we should not continue running it. Or if we have reached the - // maximum number of allowed exceptions, we should stop the test run. - throw e; - } - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Logger.warn(`There was an error running the test. Continuing the test run. Error: ${e}`); - }; - - const launchArgs = { - mockNetwork: true, - }; - - const iterationText = `Test '${test?.name}' [${testIndex + 1}/${tests.length}], iteration [${testIteration + 1}/${config.RUNS}]`; - const mainIterationText = `[MAIN] ${iterationText}`; - const deltaIterationText = `[DELTA] ${iterationText}`; - try { - // Run the test on the main app: - await runTestIteration(config.MAIN_APP_PACKAGE, mainIterationText, config.BRANCH_MAIN, launchArgs); - - // Run the test on the delta app: - await runTestIteration(config.DELTA_APP_PACKAGE, deltaIterationText, config.BRANCH_DELTA, launchArgs); - } catch (e) { - await onError(e as Error); - } - } - } catch (exception) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - Logger.warn(`Test ${test?.name} can not be finished due to error: ${exception}`); - clearTestResults(test as TestConfig); - } - } - - // Calculate statistics and write them to our work file - Logger.info('Calculating statics and writing results'); - await compare(results.main, results.delta, { - outputDir: config.OUTPUT_DIR, - outputFormat: 'all', - metricForTest, - skippedTests, - }); - Logger.info('Finished calculating statics and writing results, stopping the test server'); - - await server.stop(); -}; - -const run = async () => { - Logger.info('Running e2e tests'); - - try { - await runTests(); - - process.exit(0); - } catch (e) { - Logger.info('\n\nE2E test suite failed due to error:', e as string, '\nPrinting full logs:\n\n'); - - // Write logcat, meminfo, emulator info to file as well: - execSync(`adb logcat -d > ${config.OUTPUT_DIR}/logcat.txt`); - execSync(`adb shell "cat /proc/meminfo" > ${config.OUTPUT_DIR}/meminfo.txt`); - execSync(`adb shell "getprop" > ${config.OUTPUT_DIR}/emulator-properties.txt`); - - execSync(`cat ${config.LOG_FILE}`); - try { - execSync(`cat ~/.android/avd/${process.env.AVD_NAME ?? 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`); - } catch (ignoredError) { - // the error is ignored, as the file might not exist if the test - // run wasn't started with an emulator - } - process.exit(1); - } -}; - -run(); diff --git a/tests/e2e/utils/androidReversePort.ts b/tests/e2e/utils/androidReversePort.ts deleted file mode 100644 index acbb5a0757b3..000000000000 --- a/tests/e2e/utils/androidReversePort.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '../config'; -import type {PromiseWithAbort} from './execAsync'; -import execAsync from './execAsync'; - -function androidReversePort(): PromiseWithAbort { - return execAsync(`adb reverse tcp:${config.SERVER_PORT} tcp:${config.SERVER_PORT}`); -} - -export default androidReversePort; diff --git a/tests/e2e/utils/closeANRPopup.ts b/tests/e2e/utils/closeANRPopup.ts deleted file mode 100644 index 1763bc183ad4..000000000000 --- a/tests/e2e/utils/closeANRPopup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import execAsync from './execAsync'; -import type {PromiseWithAbort} from './execAsync'; - -const closeANRPopup = function (platform = 'android'): PromiseWithAbort { - if (platform !== 'android') { - throw new Error(`closeANRPopup() missing implementation for platform: ${platform}`); - } - - // Press "Enter" to close the ANR popup - return execAsync(`adb shell input keyevent KEYCODE_ENTER`); -}; - -export default closeANRPopup; diff --git a/tests/e2e/utils/execAsync.ts b/tests/e2e/utils/execAsync.ts deleted file mode 100644 index 3ac9a7e8bae3..000000000000 --- a/tests/e2e/utils/execAsync.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {exec} from 'child_process'; -import type {ChildProcess} from 'child_process'; -import * as Logger from './logger'; - -type PromiseWithAbort = Promise & { - abort?: () => void; -}; - -/** - * Executes a command none-blocking by wrapping it in a promise. - * In addition to the promise it returns an abort function. - */ -export default (command: string, env: NodeJS.ProcessEnv = {} as NodeJS.ProcessEnv): PromiseWithAbort => { - let childProcess: ChildProcess; - const promise: PromiseWithAbort = new Promise((resolve, reject) => { - const finalEnv: NodeJS.ProcessEnv = { - ...process.env, - ...env, - }; - - Logger.note(command); - - childProcess = exec( - command, - { - maxBuffer: 1024 * 1024 * 10, // Increase max buffer to 10MB, to avoid errors - env: finalEnv, - }, - (error, stdout) => { - if (error) { - if (error?.killed) { - resolve(); - } else { - Logger.error(`failed with error: ${error.message}`); - reject(error); - } - } else { - Logger.writeToLogFile(stdout); - resolve(stdout); - } - }, - ); - }); - - promise.abort = () => { - childProcess.kill('SIGINT'); - }; - - return promise; -}; - -export type {PromiseWithAbort}; diff --git a/tests/e2e/utils/getCurrentBranchName.ts b/tests/e2e/utils/getCurrentBranchName.ts deleted file mode 100644 index 7ae958b08e13..000000000000 --- a/tests/e2e/utils/getCurrentBranchName.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {execSync} from 'child_process'; - -const getCurrentBranchName = (): string => { - const stdout = execSync('git rev-parse --abbrev-ref HEAD', { - encoding: 'utf8', - }); - return stdout.trim(); -}; - -export default getCurrentBranchName; diff --git a/tests/e2e/utils/installApp.ts b/tests/e2e/utils/installApp.ts deleted file mode 100644 index 38bdf281c2a6..000000000000 --- a/tests/e2e/utils/installApp.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type {ExecException} from 'child_process'; -import execAsync from './execAsync'; -import type {PromiseWithAbort} from './execAsync'; -import * as Logger from './logger'; - -/** - * Installs the app on the currently connected device for the given platform. - * It removes the app first if it already exists, so it's a clean installation. - */ -export default function (packageName: string, path: string, platform = 'android', flag = ''): PromiseWithAbort { - if (platform !== 'android') { - throw new Error(`installApp() missing implementation for platform: ${platform}`); - } - - const installCommand = flag ? `adb install ${flag}` : 'adb install'; - - // Uninstall first, then install - return ( - execAsync(`adb uninstall ${packageName}`) - .catch((error: ExecException) => { - // Ignore errors - Logger.warn('Failed to uninstall app:', error.message); - }) - // install and grant push notifications permissions right away (the popup may block e2e tests sometimes) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - .finally(() => - // install the app - execAsync(`${installCommand} ${path}`).then(() => - // and grant push notifications permissions right away (the popup may block e2e tests sometimes) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - execAsync(`adb shell pm grant ${packageName.split('/').at(0)} android.permission.POST_NOTIFICATIONS`).catch((_: ExecException) => - // in case of error - just log it and continue (if we request this permission on Android < 13 it'll fail because there is no such permission) - Logger.warn( - 'Failed to grant push notifications permissions. It might be due to the fact that push-notifications permission type is not supported on this OS version yet. Continue tests execution...', - ), - ), - ), - ) - ); -} diff --git a/tests/e2e/utils/killApp.ts b/tests/e2e/utils/killApp.ts deleted file mode 100644 index f2d7bb56112f..000000000000 --- a/tests/e2e/utils/killApp.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '../config'; -import execAsync from './execAsync'; -import type {PromiseWithAbort} from './execAsync'; - -const killApp = function (platform = 'android', packageName = config.MAIN_APP_PACKAGE): PromiseWithAbort { - if (platform !== 'android') { - throw new Error(`killApp() missing implementation for platform: ${platform}`); - } - - // Use adb to kill the app - return execAsync(`adb shell am force-stop ${packageName}`); -}; - -export default killApp; diff --git a/tests/e2e/utils/launchApp.ts b/tests/e2e/utils/launchApp.ts deleted file mode 100644 index cd54a094a97c..000000000000 --- a/tests/e2e/utils/launchApp.ts +++ /dev/null @@ -1,16 +0,0 @@ -import config from '../config'; -import execAsync from './execAsync'; - -const launchApp = (platform = 'android', packageName = config.MAIN_APP_PACKAGE, activityPath = config.ACTIVITY_PATH, launchArgs: Record = {}) => { - if (platform !== 'android') { - throw new Error(`launchApp() missing implementation for platform: ${platform}`); - } - - // Use adb to start the app - const launchArgsString = Object.keys(launchArgs) - .map((key) => `${typeof launchArgs[key] === 'boolean' ? '--ez' : '--es'} ${key} ${launchArgs[key]}`) - .join(' '); - return execAsync(`adb shell am start -n ${packageName}/${activityPath} ${launchArgsString}`); -}; - -export default launchApp; diff --git a/tests/e2e/utils/logger.ts b/tests/e2e/utils/logger.ts deleted file mode 100644 index 02b91ef33b3f..000000000000 --- a/tests/e2e/utils/logger.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable import/no-import-module-exports */ -import fs from 'fs'; -import path from 'path'; -import CONFIG from '../config'; - -const COLOR_DIM = '\x1b[2m'; -const COLOR_RESET = '\x1b[0m'; -const COLOR_YELLOW = '\x1b[33m'; -const COLOR_RED = '\x1b[31m'; -const COLOR_GREEN = '\x1b[32m'; - -const getDateString = () => `[${Date()}] `; - -const writeToLogFile = (...args: string[]) => { - if (!fs.existsSync(CONFIG.LOG_FILE)) { - // Check that the directory exists - const logDir = path.dirname(CONFIG.LOG_FILE); - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir); - } - - fs.writeFileSync(CONFIG.LOG_FILE, ''); - } - fs.appendFileSync( - CONFIG.LOG_FILE, - `${args - .map((arg) => { - if (typeof arg === 'string') { - // Remove color codes from arg, because they are not supported in log files - // eslint-disable-next-line no-control-regex - return arg.replaceAll(/\x1b\[\d+m/g, ''); - } - return arg; - }) - .join(' ') - .trim()}\n`, - ); -}; - -const log = (...args: string[]) => { - const argsWithTime = [getDateString(), ...args]; - console.debug(...argsWithTime); - writeToLogFile(...argsWithTime); -}; - -const info = (...args: string[]) => { - log('▶️', ...args); -}; - -const success = (...args: string[]) => { - const lines = ['✅', COLOR_GREEN, ...args, COLOR_RESET]; - log(...lines); -}; - -const warn = (...args: string[]) => { - const lines = ['⚠️', COLOR_YELLOW, ...args, COLOR_RESET]; - log(...lines); -}; - -const note = (...args: string[]) => { - const lines = [COLOR_DIM, ...args, COLOR_RESET]; - log(...lines); -}; - -const error = (...args: string[]) => { - const lines = ['🔴', COLOR_RED, ...args, COLOR_RESET]; - log(...lines); -}; - -export {log, info, warn, note, error, success, writeToLogFile}; diff --git a/tests/e2e/utils/measure.ts b/tests/e2e/utils/measure.ts deleted file mode 100644 index ba9e7e4a22f8..000000000000 --- a/tests/e2e/utils/measure.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {profiler} from '@perf-profiler/profiler'; -import {getAverageCpuUsage, getAverageCpuUsagePerProcess, getAverageFPSUsage, getAverageRAMUsage} from '@perf-profiler/reporter'; -import {ThreadNames} from '@perf-profiler/types'; -import type {Measure} from '@perf-profiler/types'; -import * as Logger from './logger'; - -let measures: Measure[] = []; -const POLLING_STOPPED = { - stop: (): void => { - throw new Error('Cannot stop polling on a stopped profiler'); - }, -}; -let polling = POLLING_STOPPED; - -type StartOptions = { - onAttachFailed: () => Promise; -}; - -const start = (bundleId: string, {onAttachFailed}: StartOptions) => { - // clear our measurements results - measures = []; - - polling = profiler.pollPerformanceMeasures(bundleId, { - onMeasure: (measure: Measure) => { - measures.push(measure); - }, - onPidChanged: () => { - onAttachFailed(); - }, - }); - - Logger.info(`Starting performance measurements for ${bundleId}`); -}; - -const stop = (whoTriggered: string) => { - Logger.info(`Stop performance measurements... Was triggered by ${whoTriggered}`); - polling.stop(); - polling = POLLING_STOPPED; - - const average = getAverageCpuUsagePerProcess(measures); - const uiThread = average.find(({processName}) => processName === ThreadNames.ANDROID.UI)?.cpuUsage; - // most likely this line needs to be updated when we migrate to RN 0.74 with bridgeless mode - const jsThread = average.find(({processName}) => processName === ThreadNames.RN.JS_ANDROID)?.cpuUsage; - const cpu = getAverageCpuUsage(measures); - const fps = getAverageFPSUsage(measures); - const ram = getAverageRAMUsage(measures); - - return { - uiThread, - jsThread, - cpu, - fps, - ram, - }; -}; - -export {start, stop}; diff --git a/tests/e2e/utils/sleep.ts b/tests/e2e/utils/sleep.ts deleted file mode 100644 index c3f7142a898f..000000000000 --- a/tests/e2e/utils/sleep.ts +++ /dev/null @@ -1,7 +0,0 @@ -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -export default sleep; diff --git a/tests/e2e/utils/withFailTimeout.ts b/tests/e2e/utils/withFailTimeout.ts deleted file mode 100644 index b7736e75d92b..000000000000 --- a/tests/e2e/utils/withFailTimeout.ts +++ /dev/null @@ -1,36 +0,0 @@ -import CONFIG from '../config'; - -// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case -const TIMEOUT = Number(process.env.INTERACTION_TIMEOUT || CONFIG.INTERACTION_TIMEOUT); - -type WithFailTimeoutReturn = { - promise: Promise; - resetTimeout: () => void; -}; - -const withFailTimeout = (promise: Promise, name: string): WithFailTimeoutReturn => { - let timeoutId: NodeJS.Timeout; - const resetTimeout = () => { - clearTimeout(timeoutId); - }; - const race = new Promise((resolve, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(`"${name}": Interaction timed out after ${(TIMEOUT / 1000).toFixed(0)}s`)); - }, TIMEOUT); - - promise - .then((value) => { - resolve(value); - }) - .catch((e) => { - reject(e); - }) - .finally(() => { - resetTimeout(); - }); - }); - - return {promise: race, resetTimeout}; -}; - -export default withFailTimeout; diff --git a/tests/unit/E2EMarkdownTest.ts b/tests/unit/E2EMarkdownTest.ts deleted file mode 100644 index bbf33cc60a24..000000000000 --- a/tests/unit/E2EMarkdownTest.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {compareResults} from '../e2e/compare/compare'; -import {buildMarkdown} from '../e2e/compare/output/markdown'; - -const results = { - main: { - commentLinking: [100.5145680010319, 121.8861090019345, 112.0048420019448, 124.26110899820924, 135.1571460030973, 140.33837900310755, 160.7034499980509, 158.5825610011816], - }, - delta: { - commentLinking: [361.5145680010319, 402.8861090019345, 412.0048420019448, 414.26110899820924, 425.1571460030973, 440.33837900310755, 458.7034499980509, 459.5825610011816], - }, -}; - -describe('markdown formatter', () => { - it('should format significant changes properly', () => { - const data = compareResults(results.main, results.delta, {commentLinking: 'ms'}); - const markdown = buildMarkdown(data, []).join('\nEOF\n\n'); - expect(markdown).toMatchSnapshot(); - }); -}); diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 2e91198ef136..805fdb8f3412 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import type {OnyxKey, OnyxUpdate} from 'react-native-onyx'; -import {waitForActiveRequestsToBeEmpty} from '@libs/E2E/utils/NetworkInterceptor'; import {getAll, getLength, getOngoingRequest} from '@userActions/PersistedRequests'; import ONYXKEYS from '@src/ONYXKEYS'; import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; @@ -217,7 +216,7 @@ describe('SequentialQueue', () => { }); await Promise.resolve(); - await waitForActiveRequestsToBeEmpty(); + await Promise.resolve(); const persistedRequests = getAll(); // We know ReconnectApp is at index 9 in the queue, so we can get it to verify diff --git a/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap b/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap deleted file mode 100644 index c82c0f9c045d..000000000000 --- a/tests/unit/__snapshots__/E2EMarkdownTest.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`markdown formatter should format significant changes properly 1`] = ` -"## Performance Comparison Report 📊 - -### Significant Changes To Duration -| Name | Duration | -| -------------- | --------------------------------------------------- | -| commentLinking | 131.681 ms → 421.806 ms (+290.125 ms, +220.3%) 🔴🔴 | -
-Show details - -| Name | Duration | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| commentLinking | **Baseline**
Mean: 131.681 ms
Stdev: 19.883 ms (15.1%)
Runs: 100.5145680010319 112.0048420019448 121.8861090019345 124.26110899820924 135.1571460030973 140.33837900310755 158.5825610011816 160.7034499980509

**Current**
Mean: 421.806 ms
Stdev: 30.185 ms (7.2%)
Runs: 361.5145680010319 402.8861090019345 412.0048420019448 414.26110899820924 425.1571460030973 440.33837900310755 458.7034499980509 459.5825610011816 | -
- - - -### Meaningless Changes To Duration -_There are no entries_ -" -`;