diff --git a/.github/actions/cache-cocoapods/action.yml b/.github/actions/cache-cocoapods/action.yml new file mode 100644 index 0000000000..d2cec001e8 --- /dev/null +++ b/.github/actions/cache-cocoapods/action.yml @@ -0,0 +1,17 @@ +name: 'Cache CocoaPods' +description: 'Cache CocoaPods dependencies for sample_app iOS builds' +inputs: + key-suffix: + description: 'Cache key suffix (e.g. runner.os or iOS version from matrix)' + required: true +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: | + sample_app/ios/Pods + sample_app/ios/Podfile.lock + ~/Library/Caches/CocoaPods + key: pods-${{ inputs.key-suffix }}-${{ hashFiles('sample_app/pubspec.lock', 'sample_app/ios/Podfile') }} + restore-keys: pods-${{ inputs.key-suffix }}- diff --git a/.github/actions/cache-gradle/action.yml b/.github/actions/cache-gradle/action.yml new file mode 100644 index 0000000000..3a70f83236 --- /dev/null +++ b/.github/actions/cache-gradle/action.yml @@ -0,0 +1,13 @@ +name: 'Cache Gradle' +description: 'Cache Gradle dependencies for sample_app Android builds' +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + sample_app/android/.gradle + key: gradle-${{ runner.os }}-${{ hashFiles('sample_app/android/gradle/wrapper/gradle-wrapper.properties', 'sample_app/android/app/build.gradle', 'sample_app/android/build.gradle', 'sample_app/android/settings.gradle', '**/pubspec.lock') }} + restore-keys: gradle-${{ runner.os }}- diff --git a/.github/actions/cache-xcode-cas/action.yml b/.github/actions/cache-xcode-cas/action.yml new file mode 100644 index 0000000000..630e579060 --- /dev/null +++ b/.github/actions/cache-xcode-cas/action.yml @@ -0,0 +1,25 @@ +name: 'Cache Xcode CAS' +description: 'Persist Xcode compilation cache (LLVM CAS) across CI runs' +inputs: + key-suffix: + description: 'Cache key suffix (e.g. runner.os or iOS version from matrix)' + required: true + xcode-version: + description: 'Xcode version for the cache key' + required: false + default: '26.2' +runs: + using: "composite" + steps: + - shell: bash + run: | + echo "FLUTTER_XCODE_COMPILATION_CACHE_ENABLE_CACHING=YES" >> "$GITHUB_ENV" + echo "FLUTTER_XCODE_SWIFT_ENABLE_COMPILE_CACHE=YES" >> "$GITHUB_ENV" + echo "FLUTTER_XCODE_CLANG_ENABLE_COMPILE_CACHE=YES" >> "$GITHUB_ENV" + echo "FLUTTER_XCODE_COMPILATION_CACHE_CAS_PATH=$HOME/xcode-cas" >> "$GITHUB_ENV" + mkdir -p "$HOME/xcode-cas" + - uses: actions/cache@v6 + with: + path: ~/xcode-cas + key: xcode-cas-${{ inputs.xcode-version }}-${{ inputs.key-suffix }}-${{ hashFiles('sample_app/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/ios/Runner.xcodeproj/project.pbxproj') }} + restore-keys: xcode-cas-${{ inputs.xcode-version }}-${{ inputs.key-suffix }}- diff --git a/.github/actions/enable-kvm/action.yml b/.github/actions/enable-kvm/action.yml new file mode 100644 index 0000000000..82c4148797 --- /dev/null +++ b/.github/actions/enable-kvm/action.yml @@ -0,0 +1,11 @@ +name: 'Enable KVM' +description: 'Configure KVM permissions for Android emulator on Linux runners' +runs: + using: "composite" + steps: + - name: Enable KVM + shell: bash + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm diff --git a/.github/actions/package_analysis/action.yml b/.github/actions/package_analysis/action.yml index 214559aba3..79df52dd9a 100644 --- a/.github/actions/package_analysis/action.yml +++ b/.github/actions/package_analysis/action.yml @@ -1,4 +1,5 @@ name: Package Analysis +description: Package Analysis runs: using: "composite" diff --git a/.github/actions/pana/action.yml b/.github/actions/pana/action.yml index 1731910d8b..5f31fa30a6 100644 --- a/.github/actions/pana/action.yml +++ b/.github/actions/pana/action.yml @@ -1,4 +1,5 @@ name: Pana Workflow +description: 'Run pana package analysis and verify the score meets the minimum threshold' inputs: min_score: @@ -28,7 +29,7 @@ runs: yq eval '.dependencies.stream_chat = {"path": "../stream_chat"}' -i packages/stream_chat_flutter_core/pubspec.yaml yq eval '.dependencies.stream_chat_flutter_core = {"path": "../stream_chat_flutter_core"}' -i packages/stream_chat_flutter/pubspec.yaml yq eval '.dependencies.stream_chat = {"path": "../stream_chat"}' -i packages/stream_chat_persistence/pubspec.yaml - + - name: Install Flutter uses: subosito/flutter-action@v2 with: diff --git a/.github/actions/setup-flutter/action.yml b/.github/actions/setup-flutter/action.yml new file mode 100644 index 0000000000..eb4b5434c6 --- /dev/null +++ b/.github/actions/setup-flutter/action.yml @@ -0,0 +1,12 @@ +name: 'Setup Flutter' +description: 'Install Flutter with dependency caching' +runs: + using: "composite" + steps: + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.x" + channel: stable + cache: true + cache-key: "flutter-:os:-:channel:-:version:-:arch:" + pub-cache-key: "flutter-pub-:os:-${{ hashFiles('melos.yaml', '**/pubspec.yaml') }}" diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml new file mode 100644 index 0000000000..1f831dc6bc --- /dev/null +++ b/.github/actions/setup-ios-runtime/action.yml @@ -0,0 +1,46 @@ +name: Setup iOS Runtime +description: Download and install a requested iOS simulator runtime +inputs: + version: + description: iOS runtime version (e.g. 17.5) + required: true + device: + description: Simulator device type (e.g. iPhone 15 Pro) + required: true +runs: + using: composite + steps: + - name: Install ipsw + shell: bash + run: | + IPSW_VERSION=3.1.592 + FILE="ipsw_${IPSW_VERSION}_macOS_universal.tar.gz" + if ! command -v ipsw >/dev/null; then + wget -q "https://github.com/blacktop/ipsw/releases/download/v${IPSW_VERSION}/${FILE}" + tar -xzf "$FILE" + chmod +x ipsw + sudo mv ipsw /usr/local/bin/ + rm -f "$FILE" + fi + + - name: Setup iOS simulator runtime + shell: bash + run: sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true + + - name: Install iOS runtime + shell: bash + working-directory: sample_app/ios + run: bundle exec fastlane install_runtime ios:${{ inputs.version }} + + - name: Cleanup iOS runtime install artifacts + shell: bash + run: | + sudo rm -rfv sample_app/*.dmg || true + xcrun simctl list runtimes + + - name: Create custom iOS simulator + shell: bash + run: | + ios_version_dash=$(echo "${{ inputs.version }}" | tr '.' '-') + xcrun simctl create custom-test-device "${{ inputs.device }}" "com.apple.CoreSimulator.SimRuntime.iOS-${ios_version_dash}" + xcrun simctl list devices "${{ inputs.version }}" diff --git a/.github/actions/setup-java/action.yml b/.github/actions/setup-java/action.yml new file mode 100644 index 0000000000..9ecd8e14a3 --- /dev/null +++ b/.github/actions/setup-java/action.yml @@ -0,0 +1,9 @@ +name: 'Setup Java' +description: 'Install Java' +runs: + using: "composite" + steps: + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml new file mode 100644 index 0000000000..6bbccdabef --- /dev/null +++ b/.github/actions/setup-ruby/action.yml @@ -0,0 +1,10 @@ +name: 'Setup Ruby' +description: 'Install Ruby and Cache Gems' +runs: + using: "composite" + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app diff --git a/.github/workflows/distribute_external.yml b/.github/workflows/distribute_external.yml index 3e969e84db..3aafc53bfe 100644 --- a/.github/workflows/distribute_external.yml +++ b/.github/workflows/distribute_external.yml @@ -15,8 +15,6 @@ on: - ios - both -env: - FLUTTER_VERSION: '3.x' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -37,7 +35,7 @@ jobs: run: | # Is this a release? is_release="${{ github.event_name == 'release' }}" - + # If it's a release event, build both platforms if [[ "$is_release" == "true" ]]; then echo "Building all platforms due to release" @@ -45,11 +43,11 @@ jobs: echo "run_android=true" >> $GITHUB_OUTPUT exit 0 fi - + # For manual workflow dispatch, store platform in a variable platform="${{ github.event.inputs.platform }}" echo "Selected platform: $platform" - + # Set outputs only if they should be true [[ "$platform" == "ios" || "$platform" == "both" ]] && echo "run_ios=true" >> $GITHUB_OUTPUT || true [[ "$platform" == "android" || "$platform" == "both" ]] && echo "run_android=true" >> $GITHUB_OUTPUT || true @@ -69,18 +67,9 @@ jobs: with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app/android + - uses: ./.github/actions/setup-ruby - name: Distribute to S3 working-directory: sample_app/android @@ -110,21 +99,12 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app/ios + - uses: ./.github/actions/setup-ruby - name: Distribute to TestFlight working-directory: sample_app/ios diff --git a/.github/workflows/distribute_internal.yml b/.github/workflows/distribute_internal.yml index f522af2dc8..3dc85cb928 100644 --- a/.github/workflows/distribute_internal.yml +++ b/.github/workflows/distribute_internal.yml @@ -18,8 +18,6 @@ on: - ios - both -env: - FLUTTER_VERSION: '3.x' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -40,7 +38,7 @@ jobs: run: | # Is this a branch push? is_branch_push="${{ github.event_name == 'push' }}" - + # If it's a branch push, build both platforms if [[ "$is_branch_push" == "true" ]]; then echo "Building all platforms due to branch push" @@ -48,11 +46,11 @@ jobs: echo "run_android=true" >> $GITHUB_OUTPUT exit 0 fi - + # For manual workflow dispatch, store platform in a variable platform="${{ github.event.inputs.platform }}" echo "Selected platform: $platform" - + # Set outputs only if they should be true [[ "$platform" == "ios" || "$platform" == "both" ]] && echo "run_ios=true" >> $GITHUB_OUTPUT || true [[ "$platform" == "android" || "$platform" == "both" ]] && echo "run_android=true" >> $GITHUB_OUTPUT || true @@ -73,12 +71,7 @@ jobs: with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: flutter pub global activate melos @@ -86,11 +79,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app/android + - uses: ./.github/actions/setup-ruby - name: Setup Firebase Service Account' working-directory: sample_app/android @@ -120,12 +109,7 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager @@ -136,11 +120,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app/ios + - uses: ./.github/actions/setup-ruby - name: Setup Firebase Service Account working-directory: sample_app/ios @@ -173,12 +153,7 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager @@ -189,11 +164,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app/ios + - uses: ./.github/actions/setup-ruby - name: Distribute to TestFlight Internal working-directory: sample_app/ios diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 0000000000..41e1199551 --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,130 @@ +name: e2e + +on: + workflow_dispatch: + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} + +jobs: + android: + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + ANDROID_API_LEVEL: "34" + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-java + + - uses: ./.github/actions/cache-gradle + + - uses: ./.github/actions/setup-ruby + + - uses: ./.github/actions/setup-flutter + + - uses: ./.github/actions/enable-kvm + + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap + + - name: Launch Allure TestOps + working-directory: sample_app/android + run: bundle exec fastlane allure_launch + env: + GITHUB_EVENT: ${{ toJson(github.event) }} + + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.ANDROID_API_LEVEL }} + arch: x86_64 + profile: pixel_6 + script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 + + - name: Upload Allure results + if: env.LAUNCH_ID != '' && (success() || failure()) + working-directory: sample_app/android + run: bundle exec fastlane allure_upload launch_id:$LAUNCH_ID + + - name: Allure TestOps Launch Removal + if: env.LAUNCH_ID != '' && cancelled() + working-directory: sample_app/android + run: bundle exec fastlane allure_launch_removal launch_id:$LAUNCH_ID + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: e2e-android-logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log + + ios: + runs-on: macos-15 + timeout-minutes: 40 + env: + IOS_SIMULATOR_VERSION: "26.2" + IOS_SIMULATOR_DEVICE: "iPhone 17" + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-flutter + + - uses: ./.github/actions/setup-ruby + + - uses: ./.github/actions/cache-cocoapods + with: + key-suffix: ${{ runner.os }} + + - uses: ./.github/actions/cache-xcode-cas + with: + key-suffix: ${{ runner.os }} + xcode-version: ${{ env.IOS_SIMULATOR_VERSION }} + + - name: Bootstrap + run: | + flutter precache --ios + flutter config --no-enable-swift-package-manager + flutter pub global activate melos + melos bootstrap + + - name: Launch Allure TestOps + working-directory: sample_app/ios + run: bundle exec fastlane allure_launch + env: + GITHUB_EVENT: ${{ toJson(github.event) }} + + - name: Run e2e (iOS simulator) + working-directory: sample_app/ios + run: bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.IOS_SIMULATOR_VERSION }}" device:"${{ env.IOS_SIMULATOR_DEVICE }}" + + - name: Upload Allure results + if: env.LAUNCH_ID != '' && (success() || failure()) + working-directory: sample_app/ios + run: bundle exec fastlane allure_upload launch_id:$LAUNCH_ID + + - name: Allure TestOps Launch Removal + if: env.LAUNCH_ID != '' && cancelled() + working-directory: sample_app/ios + run: bundle exec fastlane allure_launch_removal launch_id:$LAUNCH_ID + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: e2e-ios-logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml new file mode 100644 index 0000000000..4ddd07b665 --- /dev/null +++ b/.github/workflows/e2e_test_cron.yml @@ -0,0 +1,170 @@ +name: E2E Tests Nightly + +on: + schedule: + - cron: "0 1 * * 1-5" + workflow_dispatch: + inputs: + mock_server_branch: + description: "Mock server branch" + type: string + required: true + default: main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + mock_server_branch: ${{ github.event.inputs.mock_server_branch || 'main' }} + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + android: + runs-on: ubuntu-latest + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + api-level: [34, 31, 28] + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-java + + - uses: ./.github/actions/cache-gradle + + - uses: ./.github/actions/setup-ruby + + - uses: ./.github/actions/setup-flutter + + - uses: ./.github/actions/enable-kvm + + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap + + - name: Launch Allure TestOps + working-directory: sample_app/android + run: bundle exec fastlane allure_launch cron:true + + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: x86_64 + profile: pixel_6 + emulator-options: -no-snapshot-save -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect + script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 mock_server_branch:${{ env.mock_server_branch }} + + - name: Upload Allure results + if: env.LAUNCH_ID != '' && (success() || failure()) + working-directory: sample_app/android + run: bundle exec fastlane allure_upload launch_id:$LAUNCH_ID + + - name: Allure TestOps Launch Removal + if: env.LAUNCH_ID != '' && cancelled() + working-directory: sample_app/android + run: bundle exec fastlane allure_launch_removal launch_id:$LAUNCH_ID + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: e2e-nightly-android-${{ matrix.api-level }} + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log + + ios: + runs-on: macos-15 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + include: + - ios: "26.2" + device: "iPhone 17 Pro" + setup_runtime: false + - ios: "18.5" + device: "iPhone 16 Pro" + setup_runtime: false + - ios: "17.5" + device: "iPhone 15 Pro" + setup_runtime: true + env: + XCODE_VERSION: "26.2" + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-ruby + + - uses: ./.github/actions/setup-flutter + + - uses: ./.github/actions/setup-ios-runtime + if: ${{ matrix.setup_runtime }} + with: + version: ${{ matrix.ios }} + device: ${{ matrix.device }} + + - uses: ./.github/actions/cache-cocoapods + with: + key-suffix: ${{ matrix.ios }} + + - uses: ./.github/actions/cache-xcode-cas + with: + key-suffix: ${{ matrix.ios }} + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Bootstrap + run: | + flutter precache --ios + flutter config --no-enable-swift-package-manager + flutter pub global activate melos + melos bootstrap + + - name: Launch Allure TestOps + working-directory: sample_app/ios + run: bundle exec fastlane allure_launch cron:true + + - name: Run e2e (iOS simulator) + working-directory: sample_app/ios + run: bundle exec fastlane run_e2e_test platform:ios ios:${{ matrix.ios }} device:"${{ matrix.device }} (${{ matrix.ios }})" mock_server_branch:${{ env.mock_server_branch }} + + - name: Upload Allure results + if: env.LAUNCH_ID != '' && (success() || failure()) + working-directory: sample_app/ios + run: bundle exec fastlane allure_upload launch_id:$LAUNCH_ID + + - name: Allure TestOps Launch Removal + if: env.LAUNCH_ID != '' && cancelled() + working-directory: sample_app/ios + run: bundle exec fastlane allure_launch_removal launch_id:$LAUNCH_ID + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: e2e-nightly-ios-${{ matrix.ios }} + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log + + slack: + name: Slack Report + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [android, ios] + if: failure() && github.event_name == 'schedule' + steps: + - uses: 8398a7/action-slack@v3 + with: + status: cancelled + text: "You shall not pass!" + job_name: "${{ github.workflow }}: ${{ github.job }}" + fields: repo,commit,author,workflow + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_NIGHTLY_CHECKS }} diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 5877a89f16..2e40d80fb8 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,7 +2,6 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "3.x" on: pull_request: @@ -62,12 +61,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: | flutter pub global activate melos @@ -91,12 +85,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: | flutter pub global activate melos @@ -118,12 +107,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter # This step is needed due to https://github.com/actions/runner-images/issues/11279 - name: Install SQLite3 run: sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev @@ -192,12 +176,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - uses: maxim-lobanov/setup-xcode@v1 if: matrix.platform == 'ios' with: @@ -206,11 +185,7 @@ jobs: run: flutter pub global activate melos - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby and Gems - uses: ruby/setup-ruby@v1 - with: - working-directory: ${{ matrix.working-directory }} - bundler-cache: true + - uses: ./.github/actions/setup-ruby - name: "Build ${{ matrix.platform }} App" working-directory: ${{ matrix.working-directory }} run: ${{ matrix.build-command }} diff --git a/.gitignore b/.gitignore index 25467a43e0..4b58b23616 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.class *.log *.pyc +/sample_app/stream-chat-test-mock-server/ +/sample_app/allure-results/ +/sample_app/allurectl *.swp .DS_Store .atom/ @@ -79,6 +82,7 @@ GeneratedPluginRegistrant.* **/ios/Runner/GeneratedPluginRegistrant.* **/ios/Flutter/ephemeral/** Podfile.lock +**/xcshareddata/swiftpm/Package.resolved # Exceptions to iOS rules !**/ios/**/default.mode1v3 diff --git a/melos.yaml b/melos.yaml index d1044c8d47..25b148ea41 100644 --- a/melos.yaml +++ b/melos.yaml @@ -86,6 +86,7 @@ command: package_info_plus: ">=9.0.1 <11.0.0" path: ^1.9.1 path_provider: ^2.1.5 + path_provider_android: ">=2.2.5 <2.3.0" photo_manager: ^3.9.0 photo_view: ^0.15.0 rate_limiter: ^1.0.0 @@ -213,6 +214,13 @@ scripts: flutter: true dirExists: test + e2e:run: + run: melos exec -c 1 --scope="sample_app" -- "flutter test integration_test/" + description: > + Run the integration_test e2e suite. Start the mock-server driver first + (`fastlane start_mock_server`), or use the `run_e2e_test` Fastlane lane to + orchestrate the mock server and tests in one step. + update:goldens: run: melos exec -c 1 --depends-on="alchemist" -- "flutter test --tags golden --update-goldens" description: Update golden files for all packages in this project. diff --git a/sample_app/android/.ruby-version b/sample_app/.ruby-version similarity index 100% rename from sample_app/android/.ruby-version rename to sample_app/.ruby-version diff --git a/sample_app/Fastfile b/sample_app/Fastfile deleted file mode 100644 index 66d2bdf8a7..0000000000 --- a/sample_app/Fastfile +++ /dev/null @@ -1,16 +0,0 @@ -opt_out_usage - -# Have an easy way to get the root of the project -def root_path - Dir.pwd.sub(/.*\Kfastlane/, '').sub(/.*\Kandroid/, '').sub(/.*\Kios/, '').sub(/.*\K\/\//, '') -end - -# Have an easy way to run flutter tasks on the root of the project -lane :sh_on_root do |options| - Dir.chdir(root_path) { sh(options[:command]) } -end - -# Tasks to be reused on each platform flow -lane :fetch_dependencies do - sh_on_root(command: "flutter pub get --suppress-analytics") -end diff --git a/sample_app/Gemfile b/sample_app/Gemfile new file mode 100644 index 0000000000..5a26ef48ca --- /dev/null +++ b/sample_app/Gemfile @@ -0,0 +1,15 @@ +source "https://rubygems.org" + +gem "fastlane" +gem "cocoapods" +gem "multi_json" +gem "sinatra", group: :sinatra_dependencies + +group :sinatra_dependencies do + gem "eventmachine" + gem "faye-websocket" + gem "puma" + gem "rackup" +end + +eval_gemfile("fastlane/Pluginfile") diff --git a/sample_app/Gemfile.lock b/sample_app/Gemfile.lock new file mode 100644 index 0000000000..d1d247454e --- /dev/null +++ b/sample_app/Gemfile.lock @@ -0,0 +1,555 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + activesupport (7.2.3.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + apktools (0.7.5) + rubyzip (~> 2.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1267.0) + aws-sdk-core (3.254.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.227.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + cocoapods (1.17.0) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.17.0) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (~> 4.1.0) + xcodeproj (>= 1.28.1, < 2.0) + cocoapods-core (1.17.0) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.3.7) + connection_pool (3.0.2) + csv (3.3.5) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + drb (2.2.3) + emoji_regex (3.2.3) + ethon (0.18.0) + ffi (>= 1.15.0) + logger + eventmachine (1.2.7) + excon (1.5.0) + logger + faraday (1.10.6) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.1) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.4) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.4.1) + fastlane (2.237.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-aws_s3 (2.1.0) + apktools (~> 0.7) + aws-sdk-s3 (~> 1) + mime-types (~> 3.3) + fastlane-plugin-firebase_app_distribution (1.0.0) + fastlane (>= 2.232.0) + google-apis-firebaseappdistribution_v1 (>= 0.9.0) + google-apis-firebaseappdistribution_v1alpha (>= 0.12.0) + fastlane-plugin-stream_actions (0.4.6) + xctest_list (= 1.2.1) + fastlane-sirp (1.1.0) + faye-websocket (0.12.0) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.8.0) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.104.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) + mini_mime (~> 1.0) + mutex_m + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + google-apis-firebaseappdistribution_v1 (0.20.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.29.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.64.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + i18n (1.15.2) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.20.0) + jwt (3.2.0) + base64 + logger (1.7.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2026.0701) + mini_magick (4.13.2) + mini_mime (1.1.5) + minitest (5.27.0) + molinillo (0.8.0) + multi_json (1.21.1) + multipart-post (2.4.1) + mustermann (3.1.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + naturally (2.3.0) + netrc (0.11.0) + nio4r (2.7.5) + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pstore (0.2.1) + public_suffix (4.0.7) + puma (8.0.2) + nio4r (~> 2.0) + rack (3.2.6) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.3.1) + rack (>= 3) + rake (13.4.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.8.0) + rexml (3.4.4) + rouge (3.28.0) + ruby-macho (4.1.0) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + securerandom (0.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + sinatra (4.2.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + tilt (2.8.0) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.6.0) + ethon (>= 0.18.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (2.6.0) + websocket-driver (0.8.2) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + xctest_list (1.2.1) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + cocoapods + eventmachine + fastlane + fastlane-plugin-aws_s3 + fastlane-plugin-firebase_app_distribution + fastlane-plugin-stream_actions (= 0.4.6) + faye-websocket + multi_json + puma + rackup + sinatra + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + activesupport (7.2.3.1) sha256=11ebed516a43a0bb47346227a35ebae4d9427465a7c9eb197a03d5c8d283cb34 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + algoliasearch (1.27.5) sha256=26c1cddf3c2ec4bd60c148389e42702c98fdac862881dc6b07a4c0b89ffec853 + apktools (0.7.5) sha256=e15819dae3fbe7146a03bc3ea74af8e5b698022b92f5570fffb38750fc37e77a + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1267.0) sha256=15a4c6bef8c303b09a1452c675cecb1ace0238dc91e26fe4646c5d761cb2221e + aws-sdk-core (3.254.0) sha256=ee3e3220b8468a3c9e59daba18e6ec897bf5c7ce8adcc0670cfa2f1f092112fe + aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 + aws-sdk-s3 (1.227.0) sha256=552b23bf9a37c7db4957e9291543e61c8de886cf31d1d45ea3b429df0feea939 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.12) sha256=7f8b757d28dfb636e7b24fba2344ac6dd13b5b24f4b46d62573d483f211825ac + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + cocoapods (1.17.0) sha256=dacf6f11ac3b00d60e6dd326485b616935230aacf95f385d145db27bfdf284af + cocoapods-core (1.17.0) sha256=a9e3d0dd36ab1b48935236d77a15cad9171217f13c6010c8e2ae3c0f455daf5b + cocoapods-deintegrate (1.0.5) sha256=517c2a448ef563afe99b6e7668704c27f5de9e02715a88ee9de6974dc1b3f6a2 + cocoapods-downloader (2.1) sha256=bb6ebe1b3966dc4055de54f7a28b773485ac724fdf575d9bee2212d235e7b6d1 + cocoapods-plugins (1.0.0) sha256=725d17ce90b52f862e73476623fd91441b4430b742d8a071000831efb440ca9a + cocoapods-search (1.0.1) sha256=1b133b0e6719ed439bd840e84a1828cca46425ab73a11eff5e096c3b2df05589 + cocoapods-trunk (1.6.0) sha256=5f5bda8c172afead48fa2d43a718cf534b1313c367ba1194cebdeb9bfee9ed31 + cocoapods-try (1.2.0) sha256=145b946c6e7747ed0301d975165157951153d27469e6b2763c83e25c84b9defe + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0 + connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2 + eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972 + excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50 + faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 + faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 + faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 + faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 + faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 + faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 + faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 + faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa + faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.237.0) sha256=bb1e867bc070fb328741b5e6e7606d5ba596941a9ecca0478f60ef22d1009db9 + fastlane-plugin-aws_s3 (2.1.0) sha256=8ee8eb103dbf0b74dc0b5273218db27a270b60b2cf508af937dcc901e7620618 + fastlane-plugin-firebase_app_distribution (1.0.0) sha256=321f554d16194ea465c71202274c75609cf986f147fc7203901d44a1a6df736f + fastlane-plugin-stream_actions (0.4.6) sha256=8afc3b72d2c7d15792369b7e3c166dd80a03b3dbee90e590c223ebe844a31f68 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + faye-websocket (0.12.0) sha256=ad9f7dfcd0306d0a13baeee450729657661129af15bb5f38716c242484ab42e1 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + fourflusher (2.3.1) sha256=1b3de61c7c791b6a4e64f31e3719eb25203d151746bb519a0292bff1065ccaa9 + fuzzy_match (2.0.4) sha256=b5de4f95816589c5b5c3ad13770c0af539b75131c158135b3f3bbba75d0cfca5 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.104.0) sha256=3bf7f0dee23ae71e070e279848374834853204e304831cf6aa76fa435a4d6eff + google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee + google-apis-firebaseappdistribution_v1 (0.20.0) sha256=b3ae3edfaf10e70c01e4f3d5eb24bc30a8373414ee90a940934083fa55086a5c + google-apis-firebaseappdistribution_v1alpha (0.29.0) sha256=308045a6d532ff960c921aa4352c44748901d1d66cd79d878a3981fa5566a88a + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 + google-apis-storage_v1 (0.64.0) sha256=75b11afa2edcee859b84c7a6972ee4456314eeef5f762827fd6cf5c5ffaf93f2 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.1) sha256=0f7e6fc70e204cee1b2d71f1e1de2d3b349d432404197fe68ebf7fa23d0821b9 + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56 + mime-types-data (3.2026.0701) sha256=cd8811e1fb89d836499ba0582368a10ee74cef929ba956d1d5ddca045e6a730f + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5 + molinillo (0.8.0) sha256=efbff2716324e2a30bccd3eba1ff3a735f4d5d53ffddbc6a2f32c0ca9433045d + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + nap (1.1.0) sha256=949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 + nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (4.0.7) sha256=8be161e2421f8d45b0098c042c06486789731ea93dc3a896d30554ee38b573b8 + puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb + rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 + rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (3.8.0) sha256=9f2f1b0207594c7817f17f671587b8ec7587387ac6cebda6c941a802bb98a8e5 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + ruby-macho (4.1.0) sha256=23dab37f7de0fe1e14f3bfa73bebc423ae8cd1d4fdb3e5585abc45a841eca920 + ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080 + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + tilt (2.8.0) sha256=ba472eb2716fe1e04112d6d219a9dae938ec09a6a1e2ad3ecc7922e79bde3721 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + typhoeus (1.6.0) sha256=bacc41c23e379547e29801dc235cd1699b70b955a1ba3d32b2b877aa844c331d + tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + xctest_list (1.2.1) sha256=9adbcb1d03f05f486e395774604fae231db0d2ae8efa9d237b3c922b09d569e3 + +BUNDLED WITH + 4.0.12 diff --git a/sample_app/android/Gemfile b/sample_app/android/Gemfile deleted file mode 100644 index 5ece8de96f..0000000000 --- a/sample_app/android/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" -gem "multi_json" - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/sample_app/android/app/build.gradle b/sample_app/android/app/build.gradle index 5ae22c6dfb..40bdd13b68 100644 --- a/sample_app/android/app/build.gradle +++ b/sample_app/android/app/build.gradle @@ -15,7 +15,7 @@ android { // dependencies that require it until Flutter's bundled // compileSdkVersion catches up. compileSdkVersion Math.max(flutter.compileSdkVersion, 36) - ndkVersion "27.0.12077973" + ndkVersion "28.2.13676358" compileOptions { sourceCompatibility JavaVersion.VERSION_17 diff --git a/sample_app/android/build.gradle b/sample_app/android/build.gradle index c85f4efa1c..22f412dfaa 100644 --- a/sample_app/android/build.gradle +++ b/sample_app/android/build.gradle @@ -24,9 +24,6 @@ subprojects { } } -subprojects { - project.evaluationDependsOn(":app") -} tasks.register("clean", Delete) { delete rootProject.layout.buildDirectory diff --git a/sample_app/android/fastlane/Appfile b/sample_app/android/fastlane/Appfile index 56549caf73..c15b5c0956 100644 --- a/sample_app/android/fastlane/Appfile +++ b/sample_app/android/fastlane/Appfile @@ -1 +1 @@ -package_name("io.getstream.chat.android.flutter.sample") # e.g. com.krausefx.app +package_name("io.getstream.chat.android.flutter.sample") # The bundle identifier of your app diff --git a/sample_app/android/fastlane/Fastfile b/sample_app/android/fastlane/Fastfile index 8667caa3ef..e7e1aaaa5d 100644 --- a/sample_app/android/fastlane/Fastfile +++ b/sample_app/android/fastlane/Fastfile @@ -1,9 +1,10 @@ skip_docs -import '../../Fastfile' +import '../../fastlane/Fastfile' default_platform(:android) before_all do if is_ci + setup_ci setup_git_config end end @@ -56,7 +57,7 @@ platform :android do # Usage: bundle exec fastlane android distribute_to_s3 lane :distribute_to_s3 do |options| build_apk - + bucket_with_path = ENV.fetch("AWS_S3_BUCKET") bucket_name, upload_path = bucket_with_path.chomp('/').split('/', 2) diff --git a/sample_app/fastlane/Allurefile b/sample_app/fastlane/Allurefile new file mode 100644 index 0000000000..d62e395e58 --- /dev/null +++ b/sample_app/fastlane/Allurefile @@ -0,0 +1,84 @@ +require 'base64' + +allure_project_id = '135' +allure_url = 'https://streamio.testops.cloud' +allure_api_url = "#{allure_url}/api" + +lane :collect_allure_results do |options| + results_dir = options[:output] || allure_results_path + FileUtils.mkdir_p(results_dir) + + log_file = options[:log_file] || "#{root_path}/build/e2e-test.log" + unless File.exist?(log_file) + UI.important("No e2e log at #{log_file}; skipping Allure collection") + next + end + + log = File.read(log_file) + + chunks = Hash.new { |hash, key| hash[key] = {} } + log.scan(/ALLURE-RESULT::([0-9a-f-]+):(\d+):([A-Za-z0-9+\/=]+)/) do |uuid, seq, chunk| + chunks[uuid][seq.to_i] = chunk + end + + chunks.each do |uuid, parts| + base64 = parts.sort_by(&:first).map(&:last).join + File.write("#{results_dir}/#{uuid}-result.json", Base64.decode64(base64)) + end + UI.message("Collected #{chunks.size} Allure result(s) into #{results_dir}") +end + +lane :install_allurectl do + path = "#{root_path}/allurectl" + next path if File.exist?(path) + + allure_version = '2.21.1' + os = `uname -s`.strip.downcase + arch = `uname -m`.strip == 'x86_64' ? 'amd64' : 'arm64' + sh("curl -sSL -o #{path} " \ + "https://github.com/allure-framework/allurectl/releases/download/#{allure_version}/allurectl_#{os}_#{arch}") + sh("chmod +x #{path}") + path +end + +lane :allure_upload do |options| + unless Dir.exist?(allure_results_path) && Dir.children(allure_results_path).any? + UI.important("No Allure results in #{allure_results_path}; skipping upload") + next + end + + allurectl = install_allurectl + allure_args = "-e #{allure_url} --project-id #{allure_project_id} --launch-id #{options[:launch_id]}" + sh("#{allurectl} launch reopen #{options[:launch_id]} || true") # to prevent allure from uploading results to a closed launch + sh("env BRANCH_NAME='#{current_branch}' #{allurectl} upload #{allure_args} #{allure_results_path} || true") + UI.success("Check out test results in Allure TestOps: #{allure_url}/launch/#{options[:launch_id]} 🎉") +end + +lane :allure_launch do |options| + next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) + + launch_id = allure_create_launch( + url: allure_api_url, + project_id: allure_project_id, + github_run_details: github_run_details, + cron: options[:cron] + ) + + sh("echo 'LAUNCH_ID=#{launch_id}' >> $GITHUB_ENV") if is_ci +end + +lane :allure_launch_removal do |options| + allure_api(url: allure_api_url, path: "launch/#{options[:launch_id]}", http_method: 'DELETE') +end + +def github_run_details + return nil unless is_ci + + github_path = "#{ENV.fetch('GITHUB_API_URL', nil)}/repos/#{ENV.fetch('GITHUB_REPOSITORY', nil)}/actions/runs/#{ENV.fetch('GITHUB_RUN_ID', nil)}" + output = sh(command: "curl -s -H 'authorization: Bearer #{ENV.fetch('GITHUB_TOKEN', nil)}' -X GET -G #{github_path}") + JSON.parse(output) +end + +def allure_results_path + "#{root_path}/allure-results" +end diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile new file mode 100644 index 0000000000..adf4c8ae86 --- /dev/null +++ b/sample_app/fastlane/Fastfile @@ -0,0 +1,127 @@ +opt_out_usage + +require 'net/http' +require 'shellwords' +import File.expand_path('Allurefile', __dir__) + +mock_server_repo_name = 'stream-chat-test-mock-server' +mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4568' +github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-flutter' +@force_check = false + +def root_path + Dir.pwd.sub(/.*\Kfastlane/, '').sub(/.*\Kandroid/, '').sub(/.*\Kios/, '').sub(/.*\K\/\//, '') +end + +def ios_device?(device) + device.to_s.match?(/\A[0-9A-F]{8}-[0-9A-F]{4}-/i) +end + +lane :sh_on_root do |options| + Dir.chdir(root_path) { sh(options[:command]) } +end + +lane :fetch_dependencies do + sh_on_root(command: "flutter pub get --suppress-analytics") +end + +lane :start_mock_server do |options| + Dir.chdir(root_path) do + mock_server_repo = options[:local_server] || mock_server_repo_name + stop_mock_server unless is_ci + + unless options[:local_server] + branch = options[:branch].to_s.empty? ? 'main' : options[:branch] + sh("rm -rf #{mock_server_repo}") if File.directory?(mock_server_repo) + sh("git clone -b #{branch} https://github.com/#{github_repo.split('/').first}/#{mock_server_repo}.git") + end + + Dir.chdir(mock_server_repo) do + FileUtils.mkdir_p('logs') + sh("bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 &") + end + end +end + +lane :stop_mock_server do + Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil +end + +def sh_with_retries(command: nil, retries: 2, label: nil) + retries = retries.to_i + label ||= command + attempt = 0 + begin + attempt += 1 + if block_given? + yield + else + sh(command) + end + true + rescue StandardError + if attempt <= retries + UI.important("#{label} failed (attempt #{attempt}/#{retries + 1}); retrying…") + retry + end + UI.error("#{label} still failing after #{retries} retries") + false + end +end + +lane :run_e2e_test do |options| + next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) + + ios = options[:platform] == 'ios' || ios_device?(options[:device]) + device = options[:device] + + start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) + + if ios + device = prepare_simulator(device: options[:device], reset: is_ci, wait_for_boot: false) + else + serial = device ? "-s #{device} " : '' + sh("adb #{serial}logcat -c") + end + + log_file = "#{root_path}/build/e2e-test.log" + FileUtils.mkdir_p(File.dirname(log_file)) + File.write(log_file, '') + + begin + Dir.chdir(root_path) do + targets = options[:target] ? [options[:target]] : Dir['integration_test/*_test.dart'].sort + UI.user_error!('No e2e test files found') if targets.empty? + + failed = [] + targets.each do |target| + flags = [target] + flags << "-d #{device}" if device + if options[:verbose] + flags << '--verbose' + else + flags << '--quiet' + flags << "--reporter #{ENV['GITHUB_ACTIONS'] ? 'github' : 'expanded'}" + end + + ok = sh_with_retries(retries: options[:retries] || 2, label: target) do + cmd = "flutter test #{flags.join(' ')} 2>&1 | tee -a #{Shellwords.escape(log_file)}" + sh("bash -o pipefail -c #{Shellwords.escape(cmd)}") + end + failed << target unless ok + end + + UI.user_error!("E2E failed after retries: #{failed.join(', ')}") unless failed.empty? + end + ensure + collect_allure_results(log_file: log_file) rescue nil + stop_mock_server + end +end + +private_lane :sources_matrix do + { + e2e: ['sample_app', 'packages'], + ruby: ['sample_app/fastlane', 'sample_app/android/fastlane', 'sample_app/ios/fastlane'], + } +end diff --git a/sample_app/android/fastlane/Pluginfile b/sample_app/fastlane/Pluginfile similarity index 77% rename from sample_app/android/fastlane/Pluginfile rename to sample_app/fastlane/Pluginfile index 3b1aeb89e6..27d5852144 100644 --- a/sample_app/android/fastlane/Pluginfile +++ b/sample_app/fastlane/Pluginfile @@ -2,6 +2,6 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-stream_actions' +gem 'fastlane-plugin-stream_actions', '0.4.6' gem 'fastlane-plugin-firebase_app_distribution' gem 'fastlane-plugin-aws_s3' diff --git a/sample_app/integration_test/allure/allure.dart b/sample_app/integration_test/allure/allure.dart new file mode 100644 index 0000000000..b9fb9c8517 --- /dev/null +++ b/sample_app/integration_test/allure/allure.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart' show TestFailure; +import 'package:uuid/uuid.dart'; + +const allureResultMarker = 'ALLURE-RESULT::'; +const _chunkSize = 900; + +enum AllureStatus { passed, failed, broken, skipped } + +class _Step { + _Step(this.name, this.start); + + final String name; + final int start; + int? stop; + AllureStatus status = AllureStatus.passed; + Map? statusDetails; + final List<_Step> steps = []; + + Map toJson() => { + 'name': name, + 'status': status.name, + if (statusDetails != null) 'statusDetails': statusDetails, + 'stage': 'finished', + 'start': start, + 'stop': stop, + 'steps': [for (final s in steps) s.toJson()], + }; +} + +class _Result { + _Result({ + required this.uuid, + required this.name, + required this.fullName, + required this.start, + required this.labels, + }); + + final String uuid; + final String name; + final String fullName; + final int start; + int? stop; + AllureStatus status = AllureStatus.passed; + Map? statusDetails; + final List> labels; + final List<_Step> steps = []; + + Map toJson() => { + 'uuid': uuid, + 'historyId': base64.encode(utf8.encode(fullName)), + 'name': name, + 'fullName': fullName, + 'status': status.name, + if (statusDetails != null) 'statusDetails': statusDetails, + 'stage': 'finished', + 'start': start, + 'stop': stop, + 'labels': labels, + 'steps': [for (final s in steps) s.toJson()], + }; +} + +class Allure { + Allure._(); + + static final Allure instance = Allure._(); + + _Result? _result; + final List<_Step> _stepStack = []; + + int get _now => DateTime.now().millisecondsSinceEpoch; + + void startTest({ + required String name, + required String fullName, + Map labels = const {}, + }) { + _stepStack.clear(); + _result = _Result( + uuid: const Uuid().v4(), + name: name, + fullName: fullName, + start: _now, + labels: [ + for (final entry in labels.entries) {'name': entry.key, 'value': entry.value}, + ], + ); + } + + Future step(String name, Future Function() body) async { + final step = _Step(name, _now); + (_stepStack.isNotEmpty ? _stepStack.last.steps : _result?.steps)?.add(step); + _stepStack.add(step); + try { + return await body(); + } on TestFailure catch (e) { + step + ..status = AllureStatus.failed + ..statusDetails = {'message': '$e'}; + rethrow; + } catch (e) { + step + ..status = AllureStatus.broken + ..statusDetails = {'message': '$e'}; + rethrow; + } finally { + step.stop = _now; + _stepStack.removeLast(); + } + } + + void stopTest({ + required AllureStatus status, + Object? message, + StackTrace? trace, + }) { + final result = _result; + if (result == null) return; + result + ..status = status + ..stop = _now; + if (message != null) { + result.statusDetails = { + 'message': '$message', + if (trace != null) 'trace': '$trace', + }; + } + + final encoded = base64.encode(utf8.encode(jsonEncode(result.toJson()))); + for (var i = 0, seq = 0; i < encoded.length; i += _chunkSize, seq++) { + final chunk = encoded.substring(i, min(i + _chunkSize, encoded.length)); + print('$allureResultMarker${result.uuid}:$seq:$chunk'); + } + _result = null; + } +} diff --git a/sample_app/integration_test/message_list_test.dart b/sample_app/integration_test/message_list_test.dart new file mode 100644 index 0000000000..623f61ef73 --- /dev/null +++ b/sample_app/integration_test/message_list_test.dart @@ -0,0 +1,26 @@ +import 'robots/user_robot_message_list_asserts.dart'; +import 'support/step.dart'; +import 'support/stream_test_case.dart'; + +void main() { + const sampleText = 'Test'; + + streamTestWithEnv( + allureId: '11188', + description: 'message list updates when the user sends a message', + body: (env) async { + await step('GIVEN the user opens a channel', () async { + await env.userRobot.login(); + await env.userRobot.openChannel(); + }); + + await step('WHEN the participant sends a message', () async { + await env.participantRobot.sendMessage(sampleText); + }); + + await step('THEN the message is displayed', () async { + await env.userRobot.assertMessage(sampleText); + }); + }, + ); +} diff --git a/sample_app/integration_test/mock_server/data_types.dart b/sample_app/integration_test/mock_server/data_types.dart new file mode 100644 index 0000000000..447662cb06 --- /dev/null +++ b/sample_app/integration_test/mock_server/data_types.dart @@ -0,0 +1,25 @@ +enum AttachmentType { + image('image'), + video('video'), + file('file'); + + const AttachmentType(this.attachment); + + final String attachment; +} + +enum ReactionType { + love('love'), + lol('haha'), + wow('wow'), + sad('sad'), + like('like'); + + const ReactionType(this.reaction); + + final String reaction; +} + +enum MessageDeliveryStatus { read, pending, sent, failed, nil } + +const forbiddenWord = 'wth'; diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart new file mode 100644 index 0000000000..af73c6156c --- /dev/null +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; +import 'dart:io'; + +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart' show Invoker; + +class MockServer { + MockServer._(this.url, this.wsUrl); + + final String url; + final String wsUrl; + + static String get _host => Platform.isAndroid ? '10.0.2.2' : '127.0.0.1'; + + static const _driverPort = String.fromEnvironment('MOCK_DRIVER_PORT', defaultValue: '4568'); + + static const _httpTimeout = Duration(seconds: 10); + static const _pingTimeout = Duration(seconds: 2); + + // `/start` spawns a whole server process and can sit in the cold driver's + // accept backlog on a loaded CI runner, so it gets a much larger budget. + static const _driverStartTimeout = Duration(seconds: 60); + + static Future start({String? testName}) async { + final name = testName ?? _currentTestName(); + final driverUrl = 'http://$_host:$_driverPort'; + final port = (await _get('$driverUrl/start/$name', timeout: _driverStartTimeout)).trim(); + final server = MockServer._('http://$_host:$port', 'ws://$_host:$port'); + await server.waitUntilReady(); + return server; + } + + static String _currentTestName() { + final name = Invoker.current?.liveTest.test.name ?? 'flutter_test'; + return name.replaceAll(RegExp('[^A-Za-z0-9_]+'), '_'); + } + + Future stop() => _get('$url/stop').catchError((_) => ''); + + Future post(String endpoint, {String? body}) async { + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.postUrl(Uri.parse('$url/$endpoint')); + if (body != null) { + req.headers.contentType = ContentType.text; + req.write(body); + } + final res = await req.close().timeout(_httpTimeout); + await res.drain(); + } finally { + client.close(force: true); + } + } + + Future get(String endpoint) => _get('$url/$endpoint'); + + Future waitUntilReady({ + Duration timeout = const Duration(seconds: 45), + }) async { + final deadline = DateTime.now().add(timeout); + while (DateTime.now().isBefore(deadline)) { + final ready = await _statusCode( + '$url/ping', + timeout: _pingTimeout, + ).then((code) => code == 200).catchError((Object _) => false); + if (ready) return; + await Future.delayed(const Duration(milliseconds: 250)); + } + throw StateError('Mock server at $url did not become ready in $timeout'); + } + + static Future _get( + String url, { + Duration timeout = _httpTimeout, + }) async { + final client = HttpClient()..connectionTimeout = timeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(timeout); + // Await the body before the `finally` force-closes the client; a bare + // `return` runs the close first and kills the connection mid-receive. + return await res.transform(utf8.decoder).join().timeout(timeout); + } finally { + client.close(force: true); + } + } + + static Future _statusCode(String url, {Duration? timeout}) async { + final client = HttpClient()..connectionTimeout = timeout ?? _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + await res.drain(); + return res.statusCode; + } finally { + client.close(force: true); + } + } +} diff --git a/sample_app/integration_test/pages/channel_list_page.dart b/sample_app/integration_test/pages/channel_list_page.dart new file mode 100644 index 0000000000..90a1e861de --- /dev/null +++ b/sample_app/integration_test/pages/channel_list_page.dart @@ -0,0 +1,5 @@ +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +abstract final class ChannelListPage { + static const Type channelTile = StreamChannelListTile; +} diff --git a/sample_app/integration_test/pages/message_list_page.dart b/sample_app/integration_test/pages/message_list_page.dart new file mode 100644 index 0000000000..ba3ab3f5fd --- /dev/null +++ b/sample_app/integration_test/pages/message_list_page.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +abstract final class MessageListPage { + static const composer = _Composer(); +} + +final class _Composer { + const _Composer(); + + Type get inputField => StreamMessageComposerInputField; + Key get sendButton => const ValueKey('send_key'); +} diff --git a/sample_app/integration_test/robots/backend_robot.dart b/sample_app/integration_test/robots/backend_robot.dart new file mode 100644 index 0000000000..5b528e0454 --- /dev/null +++ b/sample_app/integration_test/robots/backend_robot.dart @@ -0,0 +1,50 @@ +import '../mock_server/mock_server.dart'; + +class BackendRobot { + BackendRobot(this._mockServer); + + final MockServer _mockServer; + + Future generateChannels({ + required int channelsCount, + int messagesCount = 0, + int repliesCount = 0, + String? messagesText, + String? repliesText, + }) async { + final messagesTextParam = messagesText != null ? 'messages_text=${Uri.encodeQueryComponent(messagesText)}&' : ''; + final repliesTextParam = repliesText != null ? 'replies_text=${Uri.encodeQueryComponent(repliesText)}&' : ''; + await _mockServer.post( + 'mock?' + '$messagesTextParam' + '$repliesTextParam' + 'channels=$channelsCount&' + 'messages=$messagesCount&' + 'replies=$repliesCount', + ); + return this; + } + + Future failNewMessages() async { + await _mockServer.post('fail_messages'); + return this; + } + + Future freezeNewMessages() async { + await _mockServer.post('freeze_messages'); + return this; + } + + Future revokeToken({int duration = 5}) => _mockServer.post('jwt/revoke_token?duration=$duration'); + + Future invalidateToken({int duration = 5}) => _mockServer.post('jwt/invalidate_token?duration=$duration'); + + Future invalidateTokenDate({int duration = 5}) => + _mockServer.post('jwt/invalidate_token_date?duration=$duration'); + + Future invalidateTokenSignature({int duration = 5}) => + _mockServer.post('jwt/invalidate_token_signature?duration=$duration'); + + Future breakTokenGeneration({int duration = 5}) => + _mockServer.post('jwt/break_token_generation?duration=$duration'); +} diff --git a/sample_app/integration_test/robots/participant_robot.dart b/sample_app/integration_test/robots/participant_robot.dart new file mode 100644 index 0000000000..4250467cfe --- /dev/null +++ b/sample_app/integration_test/robots/participant_robot.dart @@ -0,0 +1,176 @@ +import '../mock_server/data_types.dart'; +import '../mock_server/mock_server.dart'; + +class ParticipantRobot { + ParticipantRobot(this._mockServer); + + final MockServer _mockServer; + + static const name = 'Count Dooku'; + + Future startTyping() async { + await _mockServer.post('participant/typing/start'); + return this; + } + + Future startTypingInThread() async { + await _mockServer.post('participant/typing/start?thread=true'); + return this; + } + + Future stopTyping() async { + await _mockServer.post('participant/typing/stop'); + return this; + } + + Future stopTypingInThread() async { + await _mockServer.post('participant/typing/stop?thread=true'); + return this; + } + + Future readMessage() async { + await _mockServer.post('participant/read'); + return this; + } + + Future sendMessage(String text, {int delay = 0}) async { + var endpoint = 'participant/message'; + if (delay > 0) endpoint += '?delay=$delay'; + await _mockServer.post(endpoint, body: text); + return this; + } + + Future sendMessageInThread( + String text, { + bool alsoSendInChannel = false, + }) async { + await _mockServer.post( + 'participant/message?thread=true&thread_and_channel=$alsoSendInChannel', + body: text, + ); + return this; + } + + Future editMessage(String text) async { + await _mockServer.post('participant/message?action=edit', body: text); + return this; + } + + Future deleteMessage({bool hard = false}) async { + await _mockServer.post('participant/message?action=delete&hard_delete=$hard'); + return this; + } + + Future quoteMessage(String text, {bool last = true}) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post('participant/message?$quote', body: text); + return this; + } + + Future quoteMessageInThread( + String text, { + bool alsoSendInChannel = false, + bool last = true, + }) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post( + 'participant/message?$quote&thread=true&thread_and_channel=$alsoSendInChannel', + body: text, + ); + return this; + } + + Future uploadGiphy() async { + await _mockServer.post('participant/message?giphy=true'); + return this; + } + + Future uploadGiphyInThread() async { + await _mockServer.post('participant/message?giphy=true&thread=true'); + return this; + } + + Future quoteMessageWithGiphy({bool last = true}) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post('participant/message?giphy=true&$quote'); + return this; + } + + Future quoteMessageWithGiphyInThread({ + bool alsoSendInChannel = false, + bool last = true, + }) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post( + 'participant/message?giphy=true&$quote&thread=true&thread_and_channel=$alsoSendInChannel', + ); + return this; + } + + Future pinMessage() async { + await _mockServer.post('participant/message?action=pin'); + return this; + } + + Future unpinMessage() async { + await _mockServer.post('participant/message?action=unpin'); + return this; + } + + Future uploadAttachment( + AttachmentType type, { + int count = 1, + }) async { + await _mockServer.post('participant/message?${type.attachment}=$count'); + return this; + } + + Future quoteMessageWithAttachment( + AttachmentType type, { + int count = 1, + bool last = true, + }) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post( + 'participant/message?$quote&${type.attachment}=$count', + ); + return this; + } + + Future uploadAttachmentInThread( + AttachmentType type, { + int count = 1, + bool alsoSendInChannel = false, + }) async { + await _mockServer.post( + 'participant/message?${type.attachment}=$count&thread=true&thread_and_channel=$alsoSendInChannel', + ); + return this; + } + + Future quoteMessageWithAttachmentInThread( + AttachmentType type, { + int count = 1, + bool alsoSendInChannel = false, + bool last = true, + }) async { + final quote = last ? 'quote_last=true' : 'quote_first=true'; + await _mockServer.post( + 'participant/message?$quote&${type.attachment}=$count' + '&thread=true&thread_and_channel=$alsoSendInChannel', + ); + return this; + } + + Future addReaction(ReactionType type, {int delay = 0}) async { + var endpoint = 'participant/reaction?type=${type.reaction}'; + if (delay > 0) endpoint += '&delay=$delay'; + await _mockServer.post(endpoint); + return this; + } + + Future deleteReaction(ReactionType type) async { + await _mockServer.post('participant/reaction?type=${type.reaction}&delete=true'); + return this; + } +} diff --git a/sample_app/integration_test/robots/user_robot.dart b/sample_app/integration_test/robots/user_robot.dart new file mode 100644 index 0000000000..9cfcb0c17a --- /dev/null +++ b/sample_app/integration_test/robots/user_robot.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../pages/channel_list_page.dart'; +import '../pages/message_list_page.dart'; +import '../support/predefined_users.dart'; +import '../support/widget_test_extensions.dart'; + +class UserRobot { + UserRobot(this.tester); + + final WidgetTester tester; + + Future login([ + UserCredentials user = PredefinedUsers.currentUser, + ]) async { + await tester.scrollToText(user.name); + await tester.tapText(user.name); + return this; + } + + Future openChannel({int index = 0}) async { + final tile = find.byType(ChannelListPage.channelTile).at(index); + await tester.waitUntilVisible(tile); + await tester.tap(tile); + await tester.pumpAndSettle(); + return this; + } + + Future sendMessage(String text) async { + await tester.enterTextInField(MessageListPage.composer.inputField, text); + await tester.tapByKey(MessageListPage.composer.sendButton); + return this; + } +} diff --git a/sample_app/integration_test/robots/user_robot_message_list_asserts.dart b/sample_app/integration_test/robots/user_robot_message_list_asserts.dart new file mode 100644 index 0000000000..fb1fe58cf2 --- /dev/null +++ b/sample_app/integration_test/robots/user_robot_message_list_asserts.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../support/widget_test_extensions.dart'; +import 'user_robot.dart'; + +extension UserRobotMessageListAsserts on UserRobot { + Future assertMessage(String text) async { + await tester.waitUntilVisible(find.text(text)); + return this; + } +} diff --git a/sample_app/integration_test/support/predefined_users.dart b/sample_app/integration_test/support/predefined_users.dart new file mode 100644 index 0000000000..3b2a5bdceb --- /dev/null +++ b/sample_app/integration_test/support/predefined_users.dart @@ -0,0 +1,12 @@ +class UserCredentials { + const UserCredentials({required this.id, required this.name}); + + final String id; + final String name; +} + +abstract final class PredefinedUsers { + static const qaTest1 = UserCredentials(id: 'qatest1', name: 'QA test 1'); + + static const currentUser = qaTest1; +} diff --git a/sample_app/integration_test/support/step.dart b/sample_app/integration_test/support/step.dart new file mode 100644 index 0000000000..090299b9fb --- /dev/null +++ b/sample_app/integration_test/support/step.dart @@ -0,0 +1,3 @@ +import '../allure/allure.dart'; + +Future step(String description, Future Function() body) => Allure.instance.step(description, body); diff --git a/sample_app/integration_test/support/stream_test_case.dart b/sample_app/integration_test/support/stream_test_case.dart new file mode 100644 index 0000000000..97a83f6d20 --- /dev/null +++ b/sample_app/integration_test/support/stream_test_case.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart' show Invoker; + +import '../allure/allure.dart'; +import 'stream_test_env.dart'; + +void streamTest({ + String? allureId, + required String description, + required Future Function(WidgetTester tester) body, +}) { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets(description, (tester) async { + Allure.instance.startTest( + name: description, + fullName: Invoker.current?.liveTest.test.name ?? description, + labels: {if (allureId != null) 'AS_ID': allureId}, + ); + try { + await body(tester); + final pendingException = tester.binding.takeException(); + if (pendingException != null) throw pendingException; + Allure.instance.stopTest(status: AllureStatus.passed); + } on TestFailure catch (e, st) { + Allure.instance.stopTest(status: AllureStatus.failed, message: e, trace: st); + rethrow; + } catch (e, st) { + Allure.instance.stopTest(status: AllureStatus.broken, message: e, trace: st); + rethrow; + } + }); +} + +void streamTestWithEnv({ + String? allureId, + required String description, + required Future Function(StreamTestEnv env) body, +}) { + streamTest( + allureId: allureId, + description: description, + body: (tester) async { + final env = StreamTestEnv(); + // Registered before setUp so cleanup also runs when setup fails partway + // (e.g. the mock server started but the app failed to boot). + addTearDown(env.tearDown); + await env.setUp(tester); + await body(env); + }, + ); +} diff --git a/sample_app/integration_test/support/stream_test_env.dart b/sample_app/integration_test/support/stream_test_env.dart new file mode 100644 index 0000000000..765a56c10d --- /dev/null +++ b/sample_app/integration_test/support/stream_test_env.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sample_app/app.dart'; +import 'package:sample_app/auth/auth_controller.dart'; + +import '../mock_server/mock_server.dart'; +import '../robots/backend_robot.dart'; +import '../robots/participant_robot.dart'; +import '../robots/user_robot.dart'; + +class StreamTestEnv { + MockServer? _mockServer; + MockServer get mockServer => _mockServer!; + late final BackendRobot backendRobot; + late final ParticipantRobot participantRobot; + late final UserRobot userRobot; + + Future setUp(WidgetTester tester) async { + final server = _mockServer = await MockServer.start(); + backendRobot = BackendRobot(server); + participantRobot = ParticipantRobot(server); + userRobot = UserRobot(tester); + + authController.debugConnectionOverride = StreamConnectionOverride( + baseURL: server.url, + baseWsUrl: server.wsUrl, + ); + + await tester.pumpWidget(const StreamChatSampleApp()); + await tester.pumpAndSettle(); + } + + Future tearDown() async { + try { + await authController.debugReset(); + } finally { + // Null when MockServer.start() itself failed during setUp. + await _mockServer?.stop(); + } + } +} diff --git a/sample_app/integration_test/support/widget_test_extensions.dart b/sample_app/integration_test/support/widget_test_extensions.dart new file mode 100644 index 0000000000..d7a518ec14 --- /dev/null +++ b/sample_app/integration_test/support/widget_test_extensions.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension E2EWidgetTester on WidgetTester { + Future scrollToText(String text) async { + await scrollUntilVisible( + find.text(text), + 500, + scrollable: find.byType(Scrollable).first, + ); + await pumpAndSettle(); + } + + Future tapText(String text) async { + await waitUntilVisible(find.text(text)); + await tap(find.text(text)); + await pumpAndSettle(); + } + + Future tapByType(Type type, {int index = 0}) async { + final finder = find.byType(type); + await waitUntilVisible(finder.at(index)); + await tap(finder.at(index)); + await pumpAndSettle(); + } + + Future tapByKey(Key key) async { + final finder = find.byKey(key); + await waitUntilVisible(finder); + await tap(finder); + await pumpAndSettle(); + } + + Future enterTextInField(Type inputFieldType, String text) async { + final finder = find.byType(inputFieldType); + await waitUntilVisible(finder); + await enterText(finder, text); + await pumpAndSettle(); + } + + Future waitUntilVisible( + Finder finder, { + Duration timeout = const Duration(seconds: 30), + }) async { + final end = DateTime.now().add(timeout); + while (DateTime.now().isBefore(end)) { + await pump(const Duration(milliseconds: 100)); + if (finder.evaluate().isNotEmpty) { + await pumpAndSettle(); + return; + } + } + throw TestFailure('Timed out waiting for $finder'); + } +} diff --git a/sample_app/ios/.ruby-version b/sample_app/ios/.ruby-version deleted file mode 100644 index 3b47f2e4f8..0000000000 --- a/sample_app/ios/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.3.9 diff --git a/sample_app/ios/Gemfile b/sample_app/ios/Gemfile deleted file mode 100644 index 6878dcae99..0000000000 --- a/sample_app/ios/Gemfile +++ /dev/null @@ -1,8 +0,0 @@ -source "https://rubygems.org" - -gem "fastlane" -gem "cocoapods" -gem "multi_json" - -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) \ No newline at end of file diff --git a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5db441f58a..16b76ec3c8 100644 --- a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> diff --git a/sample_app/ios/Runner/Info.plist b/sample_app/ios/Runner/Info.plist index 8ef38420ce..d24bfa9106 100644 --- a/sample_app/ios/Runner/Info.plist +++ b/sample_app/ios/Runner/Info.plist @@ -37,6 +37,8 @@ NSAllowsArbitraryLoads + NSAllowsLocalNetworking + NSAppleMusicUsageDescription Used to send message attachments diff --git a/sample_app/ios/fastlane/Fastfile b/sample_app/ios/fastlane/Fastfile index 88687c7aa8..137fc272dc 100644 --- a/sample_app/ios/fastlane/Fastfile +++ b/sample_app/ios/fastlane/Fastfile @@ -1,10 +1,15 @@ skip_docs -import '../../Fastfile' +require 'json' +import '../../fastlane/Fastfile' default_platform(:ios) -before_all do +xcode_version = ENV['XCODE_VERSION'] || '26.2' + +before_all do |lane| if is_ci setup_ci + setup_git_config + select_xcode(version: xcode_version) unless [:allure_launch, :allure_upload, :allure_launch_removal].include?(lane) end end @@ -167,4 +172,11 @@ platform :ios do in_house: false, ) end -end \ No newline at end of file + + lane :install_runtime do |options| + install_ios_runtime( + version: options[:ios], + custom_script: File.join(root_path, 'scripts/install_ios_runtime.sh'), + ) + end +end diff --git a/sample_app/ios/fastlane/Pluginfile b/sample_app/ios/fastlane/Pluginfile deleted file mode 100644 index 75686ee9e9..0000000000 --- a/sample_app/ios/fastlane/Pluginfile +++ /dev/null @@ -1,6 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-stream_actions' -gem 'fastlane-plugin-firebase_app_distribution' diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index ba2fc8c5d8..72ed1cc7ad 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -101,7 +101,7 @@ class _StreamChatSampleAppState extends State void initState() { super.initState(); _notificationService.onNotificationTap = _onNotificationTap; - _notificationService.initialize(); + if (!isE2eTestRun) _notificationService.initialize(); final timeOfStartMs = DateTime.now().millisecondsSinceEpoch; diff --git a/sample_app/lib/auth/auth_controller.dart b/sample_app/lib/auth/auth_controller.dart index 644647efa6..606fe912a5 100644 --- a/sample_app/lib/auth/auth_controller.dart +++ b/sample_app/lib/auth/auth_controller.dart @@ -26,7 +26,11 @@ final _chatPersistenceClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, ); +bool get isE2eTestRun => authController.debugConnectionOverride != null; + Future _sampleAppLogHandler(LogRecord record) async { + if (isE2eTestRun) return; + if (kDebugMode) StreamChatClient.defaultLogHandler(record); // report errors to Firebase Crashlytics @@ -39,8 +43,19 @@ Future _sampleAppLogHandler(LogRecord record) async { } } -StreamChatClient _buildStreamChatClient(String apiKey) { - const logLevel = kDebugMode ? Level.INFO : Level.SEVERE; +@visibleForTesting +class StreamConnectionOverride { + const StreamConnectionOverride({this.baseURL, this.baseWsUrl}); + + final String? baseURL; + final String? baseWsUrl; +} + +StreamChatClient _buildStreamChatClient( + String apiKey, { + StreamConnectionOverride? connectionOverride, +}) { + final logLevel = connectionOverride != null ? Level.OFF : (kDebugMode ? Level.INFO : Level.SEVERE); return StreamChatClient( apiKey, logLevel: logLevel, @@ -51,8 +66,8 @@ StreamChatClient _buildStreamChatClient(String apiKey) { return error is StreamChatNetworkError && error.isRetriable; }, ), - //baseURL: 'http://:3030', - //baseWsUrl: 'ws://:8800', + baseURL: connectionOverride?.baseURL, + baseWsUrl: connectionOverride?.baseWsUrl, )..chatPersistenceClient = _chatPersistenceClient; } @@ -93,6 +108,9 @@ class AuthController extends ValueNotifier { /// The active client, or `null` before the first [connect]. StreamChatClient? get client => _client; + @visibleForTesting + StreamConnectionOverride? debugConnectionOverride; + String? _activeApiKey; PushTokenManager? _pushTokenManager; @@ -142,7 +160,10 @@ class AuthController extends ValueNotifier { _client = null; } - final client = _client ??= _buildStreamChatClient(apiKey); + final client = _client ??= _buildStreamChatClient( + apiKey, + connectionOverride: debugConnectionOverride, + ); _activeApiKey = apiKey; try { @@ -157,11 +178,13 @@ class AuthController extends ValueNotifier { ]); } - _pushTokenManager = PushTokenManager( - client: client, - iosPushProvider: _kIosPushProvider, - androidPushProvider: _kAndroidPushProvider, - )..registerDevice(); + if (!isE2eTestRun) { + _pushTokenManager = PushTokenManager( + client: client, + iosPushProvider: _kIosPushProvider, + androidPushProvider: _kAndroidPushProvider, + )..registerDevice(); + } value = Authenticated(ownUser); } catch (_) { @@ -194,6 +217,24 @@ class AuthController extends ValueNotifier { client.disconnectUser(flushChatPersistence: flushPersistence).ignore(); } + @visibleForTesting + Future debugReset() async { + _pushTokenManager?.dispose().ignore(); + _pushTokenManager = null; + + await _client?.dispose(); + _client = null; + _activeApiKey = null; + debugConnectionOverride = null; + + if (!CurrentPlatform.isWeb) { + const secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); + } + + value = const Unauthenticated(); + } + @override void dispose() { _pushTokenManager?.dispose().ignore(); diff --git a/sample_app/pubspec.yaml b/sample_app/pubspec.yaml index b7600d33e6..1f647efe6c 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: image_picker: ^1.2.2 latlong2: ^0.9.1 lottie: ^3.3.3 + path_provider_android: ">=2.2.5 <2.3.0" rxdart: ^0.28.0 stream_chat_flutter: ^10.1.0 stream_chat_localizations: ^10.1.0 @@ -50,6 +51,8 @@ dependencies: dev_dependencies: flutter_launcher_icons: ^0.14.4 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/sample_app/scripts/install_ios_runtime.sh b/sample_app/scripts/install_ios_runtime.sh new file mode 100755 index 0000000000..e3701d3cef --- /dev/null +++ b/sample_app/scripts/install_ios_runtime.sh @@ -0,0 +1,57 @@ +#!/bin/bash -e + +log() { echo "👉 ${1}" >&2; } +die() { log "${1}"; exit 1; } +[ $# -eq 1 ] || die "usage: $0 path/to/runtime.dmg" + +dmg=$1 +mountpoint=$(mktemp -d) +staging=$(mktemp -d) + +cleanup() { + if [ -d "$staging" ]; then + set +e + log "Removing $staging..." + rm -r "$staging" || true + log "Unmounting $mountpoint..." + hdiutil detach "$mountpoint" >&2 || true + fi + + if [ -d "$mountpoint" ]; then + log "Removing $mountpoint..." + rmdir "$mountpoint" || true + fi +} +trap cleanup EXIT + +log "Mounting $dmg on $mountpoint..." +hdiutil attach "$dmg" -mountpoint "$mountpoint" >&2 + +if ! ls "$mountpoint"/*.pkg >/dev/null 2>&1; then + log "Detected a modern volume runtime; installing with simctl..." + xcrun simctl runtime add "$1" + exit 0 +fi + +log "Detected packaged runtime." + +bundle=$(echo "$mountpoint"/*.pkg) +basename=$(basename "$bundle") +sdkname=${basename%.*} +log "Found package $bundle (sdk $sdkname)." + +log "Expanding package $bundle to $staging/expanded..." +pkgutil --expand "$bundle" "$staging/expanded" + +dest=/Library/Developer/CoreSimulator/Profiles/Runtimes/$sdkname.simruntime +log "Rewriting package install location to $dest..." +sed -I '' "s|