From 6bb1ce45c47076c8dbeae9c119e3ff9b831221f5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 18 Jun 2026 16:45:05 +0100 Subject: [PATCH 01/33] test: add e2e testing infrastructure (iOS + Android) --- .github/actions/setup-ios-runtime/action.yml | 40 + .github/workflows/e2e_test.yml | 138 +++ .github/workflows/e2e_test_cron.yml | 221 +++++ .gitignore | 8 + melos.yaml | 10 + sample_app/Allurefile | 83 ++ sample_app/Fastfile | 118 +++ sample_app/android/Gemfile.lock | 266 ++++++ sample_app/android/app/build.gradle | 5 +- .../flutter/sample/MainActivityTest.java | 35 + sample_app/android/build.gradle | 3 - sample_app/integration_test/PLAN.md | 889 ++++++++++++++++++ .../integration_test/allure/allure.dart | 140 +++ .../integration_test/message_list_test.dart | 30 + .../mock_server/data_types.dart | 25 + .../mock_server/mock_server.dart | 89 ++ .../pages/channel_list_page.dart | 5 + .../pages/message_list_page.dart | 13 + .../robots/backend_robot.dart | 56 ++ .../robots/participant_robot.dart | 176 ++++ .../integration_test/robots/user_robot.dart | 33 + .../user_robot_message_list_asserts.dart | 8 + .../support/predefined_users.dart | 12 + sample_app/integration_test/support/step.dart | 4 + .../support/stream_test_case.dart | 31 + .../support/stream_test_env.dart | 37 + sample_app/ios/Gemfile.lock | 329 +++++++ sample_app/ios/Podfile | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 326 +++++-- .../xcshareddata/swiftpm/Package.resolved | 184 ++++ .../xcshareddata/xcschemes/Runner.xcscheme | 12 +- .../xcshareddata/swiftpm/Package.resolved | 184 ++++ sample_app/ios/RunnerUITests/RunnerUITests.m | 5 + sample_app/ios/add_patrol_target.rb | 58 ++ sample_app/lib/app.dart | 3 +- sample_app/lib/auth/auth_controller.dart | 85 +- sample_app/pubspec.yaml | 13 + sample_app/tool/boot_ios_simulator.sh | 75 ++ sample_app/tool/install_ios_runtime.sh | 59 ++ .../tool/patch_patrol_cli_android_gradle.sh | 145 +++ 40 files changed, 3854 insertions(+), 104 deletions(-) create mode 100644 .github/actions/setup-ios-runtime/action.yml create mode 100644 .github/workflows/e2e_test.yml create mode 100644 .github/workflows/e2e_test_cron.yml create mode 100644 sample_app/Allurefile create mode 100644 sample_app/android/Gemfile.lock create mode 100644 sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java create mode 100644 sample_app/integration_test/PLAN.md create mode 100644 sample_app/integration_test/allure/allure.dart create mode 100644 sample_app/integration_test/message_list_test.dart create mode 100644 sample_app/integration_test/mock_server/data_types.dart create mode 100644 sample_app/integration_test/mock_server/mock_server.dart create mode 100644 sample_app/integration_test/pages/channel_list_page.dart create mode 100644 sample_app/integration_test/pages/message_list_page.dart create mode 100644 sample_app/integration_test/robots/backend_robot.dart create mode 100644 sample_app/integration_test/robots/participant_robot.dart create mode 100644 sample_app/integration_test/robots/user_robot.dart create mode 100644 sample_app/integration_test/robots/user_robot_message_list_asserts.dart create mode 100644 sample_app/integration_test/support/predefined_users.dart create mode 100644 sample_app/integration_test/support/step.dart create mode 100644 sample_app/integration_test/support/stream_test_case.dart create mode 100644 sample_app/integration_test/support/stream_test_env.dart create mode 100644 sample_app/ios/Gemfile.lock create mode 100644 sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 sample_app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 sample_app/ios/RunnerUITests/RunnerUITests.m create mode 100644 sample_app/ios/add_patrol_target.rb create mode 100755 sample_app/tool/boot_ios_simulator.sh create mode 100755 sample_app/tool/install_ios_runtime.sh create mode 100755 sample_app/tool/patch_patrol_cli_android_gradle.sh diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml new file mode 100644 index 0000000000..274b35382d --- /dev/null +++ b/.github/actions/setup-ios-runtime/action.yml @@ -0,0 +1,40 @@ +name: Setup iOS Runtime +description: Download and install a requested iOS simulator runtime (stream-chat-swift parity) +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 + working-directory: sample_app/ios + run: | + sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true + bundle exec fastlane install_runtime ios:${{ inputs.version }} + sudo rm -rfv *.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/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 0000000000..62be1c8ac5 --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,138 @@ +name: e2e + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - 'README.md' + - '**/*.png' + +permissions: + contents: read + +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +env: + flutter_version: "3.x" + # Must match setup-xcode and `patrol test --ios` (Patrol e2e on macos-26). + ios_simulator_version: "26.2" + ios_simulator_device: "iPhone 17" + +jobs: + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - 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: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/android + + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap + dart pub global activate patrol_cli + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + arch: x86_64 + profile: pixel_6 + script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 mock_server_branch:main + + - name: Upload Allure results + if: always() + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/android && bundle exec fastlane allure_upload + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-android-logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/app/reports + + ios: + runs-on: macos-26 + steps: + - uses: actions/checkout@v6 + + # Align Xcode with the iOS simulator runtime (device_info_plus 12.4+ and + # Patrol both need Xcode 26.x; terminate-app flakes without this match). + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.ios_simulator_version }} + + - 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: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/ios + + - name: Bootstrap + run: | + flutter precache --ios + flutter pub global activate melos + melos bootstrap + dart pub global activate patrol_cli + + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: | + sample_app/ios/Pods + ~/Library/Caches/CocoaPods + key: pods-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} + restore-keys: pods-${{ runner.os }}- + + - name: Boot simulator + run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" + + - name: Run e2e (iOS simulator) + env: + IOS_SIMULATOR_VERSION: ${{ env.ios_simulator_version }} + run: | + cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} mock_server_branch:main + + - name: Upload Allure results + if: always() + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/ios && bundle exec fastlane allure_upload + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-ios-logs + path: sample_app/stream-chat-test-mock-server/logs diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml new file mode 100644 index 0000000000..79157983fb --- /dev/null +++ b/.github/workflows/e2e_test_cron.yml @@ -0,0 +1,221 @@ +name: E2E Tests Nightly + +on: + schedule: + - cron: "0 1 * * 1-5" # weeknights at 01:00 UTC + workflow_dispatch: + inputs: + mock_server_branch: + description: "Mock server branch" + type: string + required: true + default: main + +permissions: + contents: read + +concurrency: + group: e2e-nightly-${{ github.ref }} + cancel-in-progress: true + +env: + flutter_version: "3.x" + mock_server_branch: ${{ github.event.inputs.mock_server_branch || 'main' }} + +jobs: + setup: + if: github.repository == 'GetStream/stream-chat-flutter' + runs-on: ubuntu-latest + outputs: + launch_id: ${{ steps.launch.outputs.launch_id }} + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/android + - id: launch + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/android && bundle exec fastlane allure_launch + + android: + needs: setup + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + api-level: [34, 31, 28] + env: + ALLURE_LAUNCH_ID: ${{ needs.setup.outputs.launch_id }} + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - 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: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/android + + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap + dart pub global activate patrol_cli + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 90 + 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: success() || failure() + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/android && bundle exec fastlane allure_upload + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: e2e-nightly-android-${{ matrix.api-level }} + path: sample_app/stream-chat-test-mock-server/logs + + ios: + needs: setup + runs-on: macos-15 + 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: + ALLURE_LAUNCH_ID: ${{ needs.setup.outputs.launch_id }} + steps: + - uses: actions/checkout@v6 + + # Build with Xcode 26.2 on every matrix row; only the simulator OS varies. + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.2" + + - 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: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/ios + + - name: Bootstrap + run: | + flutter precache --ios + flutter pub global activate melos + melos bootstrap + dart pub global activate patrol_cli + + - uses: ./.github/actions/setup-ios-runtime + if: ${{ matrix.setup_runtime }} + timeout-minutes: 60 + with: + version: ${{ matrix.ios }} + device: ${{ matrix.device }} + + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: | + sample_app/ios/Pods + ~/Library/Caches/CocoaPods + key: pods-${{ matrix.ios }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} + restore-keys: pods-${{ matrix.ios }}- + + - name: Boot simulator + run: | + if [ "${{ matrix.setup_runtime }}" = "true" ]; then + bash sample_app/tool/boot_ios_simulator.sh custom-test-device + else + bash sample_app/tool/boot_ios_simulator.sh "${{ matrix.device }}" "${{ matrix.ios }}" + fi + + - name: Run e2e (iOS simulator) + env: + IOS_SIMULATOR_VERSION: ${{ matrix.ios }} + run: cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} mock_server_branch:${{ env.mock_server_branch }} + + - name: Upload Allure results + if: success() || failure() + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/ios && bundle exec fastlane allure_upload + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: e2e-nightly-ios-${{ matrix.ios }} + path: sample_app/stream-chat-test-mock-server/logs + + cleanup: + needs: [setup, android, ios] + if: always() && cancelled() && needs.setup.outputs.launch_id != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app/android + - name: Remove Allure launch + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + ALLURE_LAUNCH_ID: ${{ needs.setup.outputs.launch_id }} + run: cd sample_app/android && bundle exec fastlane allure_launch_removal + + slack: + name: Slack Report + runs-on: ubuntu-latest + needs: [android, ios] + if: failure() && github.event_name == 'schedule' + steps: + - uses: 8398a7/action-slack@v3 + with: + status: failure + text: "Nightly e2e failed πŸŒ™" + fields: repo,commit,author,workflow + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_NIGHTLY_CHECKS }} diff --git a/.gitignore b/.gitignore index 25467a43e0..7df70606ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,14 @@ *.class *.log *.pyc +# E2E: mock-server checkout cloned by the `start_mock_server` Fastlane lane +/sample_app/stream-chat-test-mock-server/ +# E2E: Patrol-generated test bundle (regenerated on every `patrol test`) +/sample_app/integration_test/test_bundle.dart +# E2E: Allure results assembled from test runs +/sample_app/allure-results/ +# E2E: allurectl binary downloaded by the Fastlane allure lanes +/sample_app/allurectl *.swp .DS_Store .atom/ diff --git a/melos.yaml b/melos.yaml index 385f3c13ea..546ba722de 100644 --- a/melos.yaml +++ b/melos.yaml @@ -125,6 +125,7 @@ command: freezed: ^3.0.0 json_serializable: ^6.13.2 mocktail: ^1.0.5 + patrol: ^4.6.1 path: ^1.9.1 path_provider_platform_interface: ^2.1.2 plugin_platform_interface: ^2.1.8 @@ -204,12 +205,21 @@ scripts: # `docs_*` are excluded β€” their goldens commit only the platform/macOS # variant and would fail on Linux CI runners. They're regenerated by # `update:goldens:docs` on a dedicated macOS workflow instead. + # Note: `flutter test` runs `test/` only, so `integration_test/` (the e2e + # suite, run via Patrol) is never picked up here. run: melos exec -c 4 --fail-fast --ignore="docs_*" -- "flutter test --coverage" description: Run Flutter tests for a specific package in this project. packageFilters: flutter: true dirExists: test + e2e:run: + run: melos exec -c 1 --depends-on="patrol" -- "patrol test" + description: > + Run the Patrol 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/Allurefile b/sample_app/Allurefile new file mode 100644 index 0000000000..ae366dccec --- /dev/null +++ b/sample_app/Allurefile @@ -0,0 +1,83 @@ +require 'base64' + +allure_endpoint = 'https://streamio.testops.cloud' +allure_project_id = '135' + +# Reassembles the chunked `ALLURE-RESULT::` markers the Dart reporter prints into +# an `allure-results/` dir for allurectl upload. The markers are read from the +# device log: logcat on Android, the simulator log on iOS (device: is a UUID). +lane :collect_allure_results do |options| + results_dir = options[:output] || "#{root_path}/allure-results" + FileUtils.mkdir_p(results_dir) + + log = if ios_device?(options[:device]) + `xcrun simctl spawn #{options[:device]} log show --last 30m --style compact 2>/dev/null` + else + serial = options[:device] ? "-s #{options[:device]} " : '' + `adb #{serial}logcat -d 2>/dev/null` + end + + 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) + + 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/latest/download/allurectl_#{os}_#{arch}") + sh("chmod +x #{path}") + path +end + +# Uploads collected allure-results to Allure TestOps. Requires ALLURE_TOKEN in +# the env. Uses ALLURE_LAUNCH_ID (or launch_id:) to upload into an existing +# launch; otherwise allurectl creates one. +lane :allure_upload do |options| + results = options[:results] || "#{root_path}/allure-results" + unless Dir.exist?(results) && Dir.children(results).any? + UI.important("No Allure results in #{results}; skipping upload") + next + end + + allurectl = install_allurectl + branch = ENV['GITHUB_REF_NAME'] || `git rev-parse --abbrev-ref HEAD`.strip + launch_id = options[:launch_id] || ENV['ALLURE_LAUNCH_ID'] + args = launch_id ? "--launch-id #{launch_id}" : "--launch-name 'Flutter e2e (#{branch})'" + sh("ALLURE_ENDPOINT=#{allure_endpoint} ALLURE_PROJECT_ID=#{allure_project_id} " \ + "#{allurectl} upload #{args} #{results}") + UI.success("Uploaded Allure results β†’ #{allure_endpoint} (project #{allure_project_id})") +end + +# Creates a launch up front and exports its id (ALLURE_LAUNCH_ID) so the nightly +# matrix jobs upload into the same launch. Requires ALLURE_TOKEN. +lane :allure_launch do + allurectl = install_allurectl + branch = ENV['GITHUB_REF_NAME'] || `git rev-parse --abbrev-ref HEAD`.strip + id = sh("ALLURE_ENDPOINT=#{allure_endpoint} ALLURE_PROJECT_ID=#{allure_project_id} " \ + "#{allurectl} launch create --launch-name 'Flutter e2e nightly (#{branch})' --format ID --no-header").strip + sh("echo 'ALLURE_LAUNCH_ID=#{id}' >> #{ENV['GITHUB_ENV']}") if ENV['GITHUB_ENV'] + sh("echo 'launch_id=#{id}' >> #{ENV['GITHUB_OUTPUT']}") if ENV['GITHUB_OUTPUT'] + id +end + +# Deletes the launch (cleanup when a nightly run is cancelled). Requires ALLURE_TOKEN. +lane :allure_launch_removal do |options| + id = options[:launch_id] || ENV['ALLURE_LAUNCH_ID'] + next unless id + + allurectl = install_allurectl + sh("ALLURE_ENDPOINT=#{allure_endpoint} ALLURE_PROJECT_ID=#{allure_project_id} " \ + "#{allurectl} launch delete #{id}") +end diff --git a/sample_app/Fastfile b/sample_app/Fastfile index 66d2bdf8a7..b813ca341b 100644 --- a/sample_app/Fastfile +++ b/sample_app/Fastfile @@ -1,10 +1,24 @@ opt_out_usage +require 'net/http' + +import '../../Allurefile' + +mock_server_repo_name = 'stream-chat-test-mock-server' +mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4568' +github_org = (ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-flutter').split('/').first +@force_check = false + # 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 +# iOS simulator ids are UUIDs; Android device ids (emulator-5554, serials) aren't. +def ios_device?(device) + device.to_s.match?(/\A[0-9A-F]{8}-[0-9A-F]{4}-/i) +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]) } @@ -14,3 +28,107 @@ end lane :fetch_dependencies do sh_on_root(command: "flutter pub get --suppress-analytics") end + +# Clones (or reuses via local_server:) the mock-server repo and starts its +# driver in the background. branch: selects the repo branch (default 'main'). +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_org}/#{mock_server_repo_name}.git") + end + + Dir.chdir(mock_server_repo) do + FileUtils.mkdir_p('logs') + Bundler.with_unbundled_env do + sh('bundle install') + sh("bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 &") + end + end + end +end + +# Stops the mock-server driver (and any servers it spawned). +lane :stop_mock_server do + Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil +end + +# Runs the Patrol e2e suite against a fresh mock server, then tears it down. +# Requires `patrol` on PATH and, on Android, a Gradle-compatible JDK. +# Options: device:, target: (single file), local_server:, mock_server_branch:. +lane :run_e2e_test do |options| + next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) + + start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) + unless ios_device?(options[:device]) + serial = options[:device] ? "-s #{options[:device]} " : '' + sh("adb #{serial}logcat -c") + end + + begin + Dir.chdir(root_path) do + # Flutter 3.44+ requires assembleDebug + assembleDebugAndroidTest in one + # Gradle invocation; patch patrol_cli before the Android build step. + unless ios_device?(options[:device]) + sh('bash tool/patch_patrol_cli_android_gradle.sh') + end + + flags = [] + flags << "--target #{options[:target]}" if options[:target] + flags << "--device #{options[:device]}" if options[:device] + flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" if ios_device?(options[:device]) + sh("patrol test #{flags.join(' ')}".strip) + end + ensure + collect_allure_results(device: options[:device]) rescue nil + stop_mock_server + end +end + +# Builds the e2e test binaries without running them (useful for CI caching). +# `platform` is android or ios. +lane :build_e2e_test do |options| + next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) + + Dir.chdir(root_path) do + sh('bash tool/patch_patrol_cli_android_gradle.sh') if options[:platform] == 'android' + sh("patrol build #{options[:platform]}") + end +end + +lane :build_and_run_e2e_test do |options| + build_e2e_test(platform: options[:platform]) + run_e2e_test(options) +end + +# Downloads and installs an iOS simulator runtime (used by cron e2e for iOS 17.x). +lane :install_runtime do |options| + install_ios_runtime( + version: options[:ios], + custom_script: File.join(root_path, 'tool/install_ios_runtime.sh'), + ) +end + +# Maps each CI check to the source paths (repo-root-relative) that should +# trigger it; `is_check_required` skips a lane when none of them changed. +private_lane :sources_matrix do + { + e2e: [ + 'sample_app', + 'packages', + 'melos.yaml', + '.github/workflows/e2e_test.yml', + '.github/workflows/e2e_test_cron.yml', + 'sample_app/tool/patch_patrol_cli_android_gradle.sh', + 'sample_app/tool/boot_ios_simulator.sh', + 'sample_app/tool/install_ios_runtime.sh', + '.github/actions/setup-ios-runtime', + ], + ruby: ['sample_app/Fastfile', 'sample_app/Allurefile', 'sample_app/android/fastlane', + 'sample_app/ios/fastlane', 'sample_app/android/Gemfile', 'sample_app/ios/Gemfile'], + } +end diff --git a/sample_app/android/Gemfile.lock b/sample_app/android/Gemfile.lock new file mode 100644 index 0000000000..1505cbe655 --- /dev/null +++ b/sample_app/android/Gemfile.lock @@ -0,0 +1,266 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + apktools (0.7.5) + rubyzip (~> 2.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1253.0) + aws-sdk-core (3.249.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.128.0) + aws-sdk-core (~> 3, >= 3.248.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.224.0) + aws-sdk-core (~> 3, >= 3.248.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) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + 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) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.5) + 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.235.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.8, < 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, < 1.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.1.0, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + 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.3) + xctest_list (= 1.2.1) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.101.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.19.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-iamcredentials_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.62.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.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.6.0) + google-cloud-storage (1.60.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.16.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.9.0) + mutex_m + jmespath (1.6.2) + json (2.19.5) + 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.0414) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + public_suffix (4.0.7) + 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) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + 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 + arm64-darwin-25 + ruby + +DEPENDENCIES + fastlane + fastlane-plugin-aws_s3 + fastlane-plugin-firebase_app_distribution + fastlane-plugin-stream_actions + multi_json + +BUNDLED WITH + 2.6.8 diff --git a/sample_app/android/app/build.gradle b/sample_app/android/app/build.gradle index 5ae22c6dfb..6e83642df6 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 @@ -33,6 +33,9 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutter.versionCode versionName flutter.versionName + + // Patrol e2e + testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" } signingConfigs { diff --git a/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java b/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java new file mode 100644 index 0000000000..e4660ed14b --- /dev/null +++ b/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java @@ -0,0 +1,35 @@ +package io.getstream.chat.android.flutter.sample; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import pl.leancode.patrol.PatrolJUnitRunner; + +// Patrol e2e entry point: enumerates the Dart `patrolTest` cases and runs each +// as a parameterized JUnit test against MainActivity. Do not edit by hand. +@RunWith(Parameterized.class) +public class MainActivityTest { + @Parameterized.Parameters(name = "{0}") + public static Object[] testCases() { + PatrolJUnitRunner instrumentation = + (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.setUp(MainActivity.class); + instrumentation.waitForPatrolAppService(); + return instrumentation.listDartTests(); + } + + public MainActivityTest(String dartTestName) { + this.dartTestName = dartTestName; + } + + private final String dartTestName; + + @org.junit.Test + public void runDartTest() { + PatrolJUnitRunner instrumentation = + (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.runDartTest(dartTestName); + } +} 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/integration_test/PLAN.md b/sample_app/integration_test/PLAN.md new file mode 100644 index 0000000000..58fecd5545 --- /dev/null +++ b/sample_app/integration_test/PLAN.md @@ -0,0 +1,889 @@ +# E2E Testing Infrastructure for Flutter (iOS + Android) + +A detailed plan to port the Stream Chat **Android** e2e testing infrastructure +to the **Flutter** SDK's `sample_app`, using the +[Patrol](https://github.com/leancodepl/patrol) framework, while reusing the +existing `stream-chat-test-mock-server` and mimicking the Android Robot / +PageObject / test DSL as closely as possible. + +The goal is a single shared test suite that runs on **both Flutter iOS and +Android** with maximal code sharing. + +--- + +## 0. TL;DR + +- Add Patrol-based integration tests to `sample_app/`. +- Recreate the Android DSL in Dart: **PageObjects** (selector definitions), + **UserRobot** (fluent UI actions + assertion extensions), **BackendRobot** / + **ParticipantRobot** (mock-server control), a **StreamTestCase** base, and + the **mock server client**. +- Reuse `stream-chat-test-mock-server` **as-is** (Ruby/Sinatra driver + + per-test server). Tests talk to it over HTTP from Dart. **Every test gets its + own isolated mock-server process** (the driver forks one per `/start/:test`). +- **Decision: Option A β€” direct in-process configuration.** Because Patrol tests + run **in-process** (Dart test code lives inside the app), point + `StreamChatClient` at the mock server **directly from Dart** via a + `debugConnectionOverride` on the `authController` singleton β€” no Intent extras + / env vars needed (the big simplification vs. Android & iOS). Options B + (`--dart-define`) and C (native bridge) stay documented as alternatives; a few + cold-start/push tests may use a B-shaped escape hatch (Β§3.5). +- Add `Gemfile` + Fastlane lanes (`build_e2e`, `run_e2e`, `start/stop_mock_server`, + Allure upload), mirroring the Android lanes. +- Wire up Allure TestOps reporting and a GitHub Actions e2e job. + +--- + +## 1. Source material (what we are copying) + +### Android (primary source) +- App-side suite: `stream-chat-android-compose-sample/src/androidTestE2eDebug/` + - `pages/` β€” PageObjects (selector definitions only) + - `robots/` β€” `UserRobot` + assertion extension files + - `tests/` β€” test classes (`StreamTestCase` base + 11 feature suites) + - `resources/allure.properties` +- Shared library: `stream-chat-android-e2e-test/src/main/kotlin/.../e2e/test/` + - `uiautomator/` β€” thin DSL over UIAutomator (Base, Element, Wait, FindBy, Actions, Math, Permissions) + - `mockserver/` β€” `MockServer`, `DataTypes` (enums) + - `robots/` β€” `BackendRobot`, `ParticipantRobot` + - `rules/` β€” `RetryRule` +- `fastlane/Fastfile`, `fastlane/Allurefile`, root `Gemfile` + +### iOS / Swift (reference for the mock-server integration approach) +- `stream-chat-swift/TestTools/StreamChatTestMockServer/` (MockServer, Robots) +- Uses env vars + launch args to pass the mock URL to the app. + +### Mock server (reused as-is) +- `stream-chat-test-mock-server/` β€” Ruby + Sinatra. + - `driver.rb` (port `4567` by default): `GET /start/:test_name` spawns a fresh + mock server and returns its port; `GET /stop` shuts the driver down. + - `src/server.rb` (dynamic port): mocks the Stream REST API **and** the + WebSocket (`/connect`) including periodic health events. + - Control endpoints used by tests: + - **Backend setup:** `POST /mock?channels=&messages=&replies=&messages_text=&attachments=`, + `/fail_messages`, `/freeze_messages`, `/delay_messages?delay=`, + `/jwt/revoke_token`, `/jwt/invalidate_token[_date|_signature]`, + `/jwt/break_token_generation`, `/config/read_events`, `/config/cooldown`, + `GET /ping`, `GET /jwt/get?platform=`. + - **Participant actions:** `POST /participant/message` (+ `action`, `thread`, + `giphy`, `image|video|file`, `delay`, `quote_first|quote_last`), + `/participant/reaction` (`type`, `delete`, `delay`), + `/participant/typing/start|stop`, `/participant/read`, `/participant/push`. + +--- + +## 2. Key architectural difference: Patrol vs. UIAutomator / XCUITest + +This is the most important thing to internalize before porting, because it +**simplifies** the design relative to Android/iOS. + +| | Android (UIAutomator) | iOS (XCUITest) | **Flutter (Patrol)** | +|---|---|---|---| +| Where test code runs | Separate instrumentation process | Separate UI-test runner process | **In-process, inside the app (Dart)** | +| How it drives UI | OS-level accessibility tree | OS-level accessibility tree | Flutter `WidgetTester` finders (+ Patrol native automation for OS dialogs) | +| How app learns the mock URL | `Intent` extra `BASE_URL` | env vars + launch args | **Direct: Dart test builds the client with the mock URL** | +| Selector strategy | `By.res("Stream_X")` resource IDs | accessibility identifiers | `find.byKey` / `patrol.($('...'))` on widget `Key`s / semantics | + +**Implications:** +1. **No IPC needed to inject the mock URL.** The Patrol test `main()` constructs + the app widget itself, so it can pass the mock `baseURL`/`baseWsUrl` straight + into `StreamChatClient`. We expose a test entrypoint (e.g. + `runStreamApp(connectionOptions)`) the test calls directly. +2. **The mock-server client (`BackendRobot`/`ParticipantRobot`) runs in Dart** + inside the same process β€” it just does HTTP calls to the driver/mock server. +3. **Selectors must be added to the production widgets.** UIAutomator relied on + `Stream_*` testTags already present in Compose. In Flutter we must add `Key`s + (or `Semantics(identifier:)`) to the relevant widgets in `stream_chat_flutter` + and/or `sample_app`. This is the largest cross-cutting code change. +4. **Patrol's native automation** still handles OS-level concerns the in-process + tester cannot: runtime permission dialogs (photos/camera/notifications), + notifications, backgrounding the app, toggling wifi/airplane mode β€” these map + to the Android `device.*` helpers (`grantPermission`, `disableInternetConnection`, + `goToBackground`, etc.). + +--- + +## 3. Mock server integration β€” DECISION + options + +> **Decision: we go with Option A (Direct in-process configuration).** Options B +> and C are documented below as alternatives / escape hatches, but A is the +> primary approach the rest of this plan assumes. A handful of tests that need a +> true OS cold start or push-tap navigation may use a B-shaped variant (see +> Β§3.5). + +Android passes the URL via an `Intent` extra; iOS passes it via environment +variables + launch arguments. Neither is necessary for Patrol because the test +runs in-process. Three viable options: + +### Option A β€” Direct in-process configuration (CHOSEN) +The Patrol test resolves the mock-server URL (via the driver) and the app reads +it when it builds its `StreamChatClient`. + +**The real seam (grounded in the current code).** The client is *not* passed +into the widget β€” `StreamChatSampleApp` takes no config (`const +StreamChatSampleApp({super.key})`), and the client is built inside a +**process-wide singleton** `authController` via the private +`_buildStreamChatClient(apiKey)` (`auth_controller.dart`, with `baseURL`/ +`baseWsUrl` currently hardcoded-and-commented at lines ~54–55). So we inject the +override onto that singleton *before* the first `connect()` runs, rather than +threading a widget parameter: + +```dart +// auth_controller.dart β€” test seam +StreamConnectionOverride? debugConnectionOverride; // null in production + +StreamChatClient _buildStreamChatClient(String apiKey) { + return StreamChatClient( + apiKey, + baseURL: debugConnectionOverride?.baseURL, // null β†’ SDK default (prod) + baseWsUrl: debugConnectionOverride?.baseWsUrl, + logLevel: logLevel, + logHandlerFunction: _sampleAppLogHandler, + retryPolicy: RetryPolicy(...), + )..chatPersistenceClient = _chatPersistenceClient; +} +``` + +```dart +patrolTest('test_addsReaction', ($) async { + final mock = await MockServer.start(testName: 'test_addsReaction'); + authController.debugConnectionOverride = StreamConnectionOverride( + baseURL: mock.httpUrl, // http://10.0.2.2: (Android) / http://localhost: (iOS) + baseWsUrl: mock.wsUrl, // ws://... + ); + await $.pumpWidgetAndSettle(const StreamChatSampleApp()); + // ... robots/asserts +}); +``` + +Because the URL is read at **client-build time** (first `connect()`), the +driver's per-test **dynamic port** works without any build-time configuration. + +**Pros** +- **No IPC** β€” the single biggest win. Android needs an `Intent` extra and iOS + needs env vars + launch args only because their runners are out-of-process; + Patrol runs in-process, so the test sets the URL by direct assignment. +- **One code path for iOS + Android** β€” the seam is pure Dart; the only platform + branch is the mock host (`10.0.2.2` vs `localhost`), which lives in + `MockServer`, not in the app. +- **Trivial robots** β€” `BackendRobot`/`ParticipantRobot` are plain `http` calls + in the same isolate as the test; no bridge, no serialization. +- **Prod-safe & tight** β€” when the override is null, behaviour is unchanged (SDK + falls back to its default base URL); the shipped diff is a few lines. +- **Per-test dynamic ports work** β€” URL resolved at runtime from the driver + (unlike Option B, which bakes a port at build time). + +**Cons** (each is one-time setup, not per-test friction) +- **Seam touches a global singleton, not a widget** β€” `authController` is a + process-wide `final`; the override is ambient global state (see next point). +- **In-process global state leaks between bundled tests** β€” Patrol runs all + tests in one app process. `authController` persists, `tryAutoConnect()` reads + `FlutterSecureStorage`, and `connect()` deliberately keeps `_client` alive + (lines ~140–145). Each test must reset in teardown (clear secure storage, + dispose/reset client, re-set override). Android sidesteps this with + `clearPackageData true` per test; we emulate it in Dart `tearDown`. See Β§6. +- **Bypasses the real cold-start path** β€” the test pumps the widget tree and + skips `main.dart` (Firebase init, push pipeline, deep-link / `_onNotificationTap` + entry). Tests that must validate a true OS cold start or push-tap navigation + don't fit A cleanly β†’ use Β§3.5. +- **Boot-path side effects can fail under test** β€” `initState` runs + `NotificationService.initialize()` and the log handler calls + `FirebaseCrashlytics.instance.recordError`. Pumping the root widget without + Firebase configured can throw; guard these behind the e2e flag (see Β§6). +- **JWT/auth suite needs extra wiring** β€” the override only covers + `baseURL`/`baseWsUrl`; the token provider must separately be redirected to the + mock server's `GET /jwt/get` for the token-expiry tests. + +### Option B β€” `--dart-define` compile-time configuration +Pass `MOCK_HOST`/`MOCK_PORT` via `--dart-define`; the app reads +`String.fromEnvironment(...)` at startup and overrides the client URL. +- **Pros:** No widget param; closest analog to iOS env-var approach; production + code path stays untouched at runtime (values baked at build). +- **Cons:** Port is dynamic per test (the driver assigns it), but `--dart-define` + is fixed at build time β€” so you'd have to pin the mock server to a **fixed + port per run** (skip the driver's per-test port allocation) or only define the + host and resolve the port at runtime anyway. Less flexible than A. + +### Option C β€” Native bridge (mimic Android/iOS exactly) +Replicate Android's `Intent`-extra / iOS's env-var mechanism through Patrol's +native layer. +- **Pros:** Most faithful to the existing mobile projects. +- **Cons:** Pointless indirection for Patrol (the test already runs in-process); + more native plumbing, two platform paths. Not recommended. + +### Option A.5 β€” escape hatch for cold-start / push tests (B-shaped) +A small minority of tests need a true OS launch (deep links, push-tap +navigation via `_onNotificationTap`). Those don't fit in-process pumping. For +them, launch the app natively via Patrol and pass the mock **host** through +`--dart-define` (the app reads `String.fromEnvironment('MOCK_HOST')` at +startup), pinning the driver/server to a known port for that run. The project +ends up **A-primary with a B-shaped escape hatch** for the few launch/push +tests. + +### Per-test mock server isolation (independent of the chosen option) +This is determined by the mock-server *driver* design, not by Option A/B/C β€” +and **yes, every test gets its own dedicated, isolated mock server instance.** + +Two processes, two roles: +1. **Driver** (`driver.rb`, one long-lived process on port `4567`) β€” started + once per CI run by Fastlane. Stateless; only spawns/tracks servers. +2. **Mock server** (`server.rb`) β€” one **fresh OS process per test**, on its own + dynamically-allocated port. + +``` +test_A β†’ GET /start/test_A β†’ driver forks server.rb β†’ port 5012 (own channels/messages/JWT state) +test_B β†’ GET /start/test_B β†’ driver forks server.rb β†’ port 5013 (completely separate) +``` + +So if `StreamTestEnv.setUp()` calls `MockServer.start(testName:)` per test (the +port of Android's `StreamTestCase`/`@Before`), no state leaks **at the server +layer**. The important caveat is the *app* side, because Patrol bundles all +tests into **one app process**: + +| Layer | Per-test fresh? | +|---|---| +| Mock server process (driver-spawned) | βœ… Yes, automatically | +| Dart app process (Patrol bundle) | ❌ No β€” shared across all tests | +| `authController` singleton, secure storage, `_client` | ❌ No β€” must be reset in `tearDown` | + +Therefore each test must (a) call `MockServer.start()` in `setUp` and re-inject +the *new* port via `debugConnectionOverride`, and (b) reset app-side globals in +`tearDown` (see Β§6). Server isolation is free; app isolation is manual. + +### Networking detail (applies to all options) +- **Android emulator** reaches the host loopback at `10.0.2.2`; **iOS simulator** + uses `localhost`/`127.0.0.1`. `MockServer` must pick the host per platform + (`Platform.isAndroid ? '10.0.2.2' : 'localhost'`). +- The mock server is plain **HTTP/WS** (not TLS). Android needs cleartext + permitted: add a debug `network_security_config` (or `usesCleartextTraffic`) + for the e2e build. The Stream client already supports `http://` base URLs; no + `forceInsecureConnection` equivalent is needed in Dart, but confirm Dio allows + cleartext on the platform. +- **JWT:** the mock server issues tokens at `GET /jwt/get?platform=`. For + parity, the sample app's token provider should fetch from the mock server when + running e2e (needed for the auth/token-expiry suite). For non-auth tests the + predefined static tokens in `app_config.dart` are sufficient. + +--- + +## 4. Target directory layout (Flutter) + +Two homes, mirroring Android's split between the shared lib +(`stream-chat-android-e2e-test`) and the app-side suite +(`androidTestE2eDebug`): + +``` +sample_app/ +β”œβ”€β”€ integration_test/ +β”‚ β”œβ”€β”€ tests/ # ← androidTestE2eDebug/tests +β”‚ β”‚ β”œβ”€β”€ stream_test_case.dart # StreamTestCase base (setUp/tearDown, robots) +β”‚ β”‚ β”œβ”€β”€ auth_test.dart +β”‚ β”‚ β”œβ”€β”€ message_list_test.dart +β”‚ β”‚ β”œβ”€β”€ reactions_test.dart +β”‚ β”‚ β”œβ”€β”€ quoted_reply_test.dart +β”‚ β”‚ β”œβ”€β”€ attachments_test.dart +β”‚ β”‚ β”œβ”€β”€ giphy_test.dart +β”‚ β”‚ β”œβ”€β”€ draft_messages_test.dart +β”‚ β”‚ β”œβ”€β”€ message_delivery_status_test.dart +β”‚ β”‚ β”œβ”€β”€ hyperlinks_test.dart +β”‚ β”‚ β”œβ”€β”€ channel_list_test.dart +β”‚ β”‚ └── backend_test.dart +β”‚ β”œβ”€β”€ robots/ # ← androidTestE2eDebug/robots +β”‚ β”‚ β”œβ”€β”€ user_robot.dart # fluent UI actions +β”‚ β”‚ β”œβ”€β”€ user_robot_channel_list_asserts.dart +β”‚ β”‚ └── user_robot_message_list_asserts.dart +β”‚ β”œβ”€β”€ pages/ # ← androidTestE2eDebug/pages +β”‚ β”‚ β”œβ”€β”€ login_page.dart +β”‚ β”‚ β”œβ”€β”€ channel_list_page.dart +β”‚ β”‚ β”œβ”€β”€ message_list_page.dart +β”‚ β”‚ β”œβ”€β”€ thread_page.dart +β”‚ β”‚ └── jwt_page.dart +β”‚ └── test_bundle.dart # Patrol bundled-test entrypoint (generated) +β”‚ +β”œβ”€β”€ test_driver/ +β”‚ └── integration_test.dart # Patrol/integration_test driver +β”‚ +└── e2e/ # shared helpers (β‰ˆ stream-chat-android-e2e-test) + β”œβ”€β”€ mock_server/ + β”‚ β”œβ”€β”€ mock_server.dart # driver client + per-test server lifecycle + β”‚ └── data_types.dart # AttachmentType / ReactionType / MessageDeliveryStatus enums + β”œβ”€β”€ robots/ + β”‚ β”œβ”€β”€ backend_robot.dart + β”‚ └── participant_robot.dart + β”œβ”€β”€ patrol_ext/ # thin DSL over Patrol (β‰ˆ uiautomator/) + β”‚ β”œβ”€β”€ waits.dart # waitToAppear/waitToDisappear/waitForText + β”‚ β”œβ”€β”€ actions.dart # typeText/longPress/swipe/background/connectivity + β”‚ └── finders.dart # selector β†’ PatrolFinder helpers + └── support/ + β”œβ”€β”€ retry.dart # retry harness (β‰ˆ RetryRule) + β”œβ”€β”€ allure.dart # Allure step() + attachments + └── predefined_users.dart # β‰ˆ PredefinedUserCredentials +``` + +> Patrol can also live in a separate package if we want the e2e suite isolated +> from `sample_app`'s own `test/`. Recommendation: keep it inside `sample_app` +> (Patrol expects `integration_test/` next to the app) and add `e2e/` as plain +> Dart source compiled into the test binary. + +--- + +## 5. Component-by-component port + +### 5.1 PageObjects (`pages/`) +Android uses nested companion objects of `By.res("Stream_*")`. In Dart, model +the same hierarchy with nested classes exposing **Patrol selectors / Keys**. + +Android: +```kotlin +class MessageListPage { + class Composer { + companion object { + val inputField = By.res("Stream_ComposerInputField") + val sendButton = By.res("Stream_ComposerSendButton") + } + } + class MessageList { + companion object { val messages = By.res("Stream_MessageCell") } + class Message { + companion object { + val text = By.res("Stream_MessageText") + class Reactions { + companion object { fun reaction(t: ReactionType): BySelector = ... } + } + } + } + } +} +``` + +Dart (keep the same names/structure so the DSL reads identically): +```dart +abstract class MessageListPage { + static const composer = _Composer(); + static const messageList = _MessageList(); +} + +class _Composer { + const _Composer(); + Key get inputField => const Key('Stream_ComposerInputField'); + Key get sendButton => const Key('Stream_ComposerSendButton'); +} + +class _MessageList { + const _MessageList(); + Key get messages => const Key('Stream_MessageCell'); + _Message get message => const _Message(); +} + +class _Message { + const _Message(); + Key get text => const Key('Stream_MessageText'); + Key reaction(ReactionType type) => Key('Stream_MessageReaction_${type.reaction}'); +} +``` + +**Required production change:** add these `Key`s to the corresponding widgets. +Build a mapping table (Compose `testTag` β†’ Flutter widget β†’ `Key`) and add keys +in `stream_chat_flutter` (and `sample_app` for app-specific screens). Prefer +`Key('Stream_*')` constants matching the Android resource-id strings so the +selector tables stay 1:1. Keep the constant strings in one shared file to avoid +drift between widget and page object. + +### 5.2 The Patrol DSL layer (`e2e/patrol_ext/` β‰ˆ `uiautomator/`) +Recreate the wait/action helpers as extensions on `PatrolTester`/`PatrolFinder`: +- `waitToAppear({Duration timeout})`, `waitToDisappear()`, `waitForText()`, + `waitForCount()` β†’ wrap `patrolTester.waitUntilVisible` / polling. +- `typeText`, `longPress`, `tap`, `swipeUp/Down`, `scrollUntilVisible`. +- Device/native (via Patrol native automator): + `grantPermission`, `disableInternetConnection`/`enableInternetConnection` + (Patrol `nativeAutomator` wifi/cellular toggles or `setAirplaneMode`), + `goToBackground`/`goToForeground`, `tapOnNotification`. +- `defaultTimeout = Duration(seconds: 5)` constant, mirroring Android. + +### 5.3 Data types (`e2e/mock_server/data_types.dart` β‰ˆ `DataTypes.kt`) +Direct port of the enums: +```dart +enum AttachmentType { image, video, file } +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'; +``` + +### 5.4 Mock server client (`e2e/mock_server/mock_server.dart` β‰ˆ `MockServer.kt`) +```dart +class MockServer { + MockServer._(this.httpUrl, this.wsUrl); + final String httpUrl; + final String wsUrl; + + // Host differs per platform (emulator loopback vs simulator localhost). + static String get _host => Platform.isAndroid ? '10.0.2.2' : 'localhost'; + + // Single Flutter driver port, distinct from the native repos (android=4567, + // swift=4566) so Flutter e2e can run alongside them. Flutter-iOS and + // Flutter-Android SHARE this port: they aren't run simultaneously on the same + // host (CI runs them as separate matrix jobs). Overridable via --dart-define + // for the rare local case of running both platforms at once. + static const _driverPort = String.fromEnvironment('MOCK_DRIVER_PORT', defaultValue: '4568'); + + static Future start({required String testName}) async { + final driver = 'http://$_host:$_driverPort'; + final res = await http.get(Uri.parse('$driver/start/$testName')); + final port = res.body.trim(); + final http_ = 'http://$_host:$port'; + final ws = 'ws://$_host:$port'; + final server = MockServer._(http_, ws); + // The driver spawns server.rb asynchronously; it needs ~0.5–1s to boot + // puma before it answers. Poll /ping until ready (verified in the Phase 0 + // spike) so the app doesn't try to connect before the server is listening. + await server._waitUntilReady(); + return server; + } + + Future _waitUntilReady({Duration timeout = const Duration(seconds: 10)}) async { + // poll `GET $httpUrl/ping` until 200 or timeout + } + + Future stop() { /* optional per-test stop */ } + Future post(String endpoint, [Object? body]) => ...; + Future get(String endpoint) => ...; +} +``` + +### 5.5 BackendRobot / ParticipantRobot (`e2e/robots/` β‰ˆ Android robots) +Faithful Dart ports of the Kotlin robots; each method is one HTTP call and +returns `this` for chaining. +```dart +class BackendRobot { + BackendRobot(this._mock); + final MockServer _mock; + + Future generateChannels({ + required int channelsCount, + int messagesCount = 0, + int repliesCount = 0, + String? messagesText, + String? repliesText, + bool attachments = false, + }) async { await _mock.post('/mock?channels=$channelsCount&messages=$messagesCount&...'); return this; } + + Future failNewMessages() async { await _mock.post('/fail_messages'); return this; } + Future freezeNewMessages() async { ... } + Future revokeToken({int duration = 5}) async { ... } + Future invalidateToken({int duration = 5}) async { ... } + // ...invalidateTokenDate / invalidateTokenSignature / breakTokenGeneration / setReadEvents / setCooldown +} + +class ParticipantRobot { + static const name = 'Count Dooku'; + Future sendMessage(String text, {int delay = 0}) async { ... } + Future sendMessageInThread(String text, {bool alsoSendInChannel = false}) async { ... } + Future editMessage(String text) async { ... } + Future deleteMessage({bool hard = false}) async { ... } + Future quoteMessage(String text, {bool last = true}) async { ... } + Future addReaction(ReactionType type, {int delay = 0}) async { ... } + Future deleteReaction(ReactionType type) async { ... } + Future startTyping({bool thread = false}) async { ... } + Future stopTyping({bool thread = false}) async { ... } + Future readMessage({String? parentId}) async { ... } +} +``` +> Dart note: the Android robots are synchronous (blocking HTTP); Dart HTTP is +> async, so robot methods return `Future<...>` and tests `await` them. Keep the +> fluent feel via `await robot.x(); await robot.y();` or chained `.then`. This is +> the one unavoidable deviation from the Kotlin DSL. + +### 5.6 UserRobot (`robots/user_robot.dart` β‰ˆ `UserRobot.kt`) +Fluent UI-action robot built on the Patrol DSL + page objects. Holds a reference +to the `PatrolTester`. Same method names as Android: `login`, `logout`, +`openChannel`, `sendMessage`, `typeText`, `editMessage`, `deleteMessage`, +`openContextMenu`, `addReaction`, `deleteReaction`, `quoteMessage`, `openThread`, +`sendMessageInThread`, `uploadAttachment`, `uploadGiphy`, `scrollMessageListUp/Down`, +`swipeMessage`, `mentionParticipant`, etc. +```dart +class UserRobot { + UserRobot(this.$); + final PatrolTester $; + + Future login() async { + await $(LoginPage.loginButton).waitToAppear().tap(); + return this; + } + Future openChannel({int index = 0}) async { ... } + Future sendMessage(String text) async { + await $(MessageListPage.composer.inputField).enterText(text); + await $(MessageListPage.composer.sendButton).tap(); + return this; + } +} +``` +Assertions go in **extension** files (`user_robot_message_list_asserts.dart`, +`user_robot_channel_list_asserts.dart`), mirroring `UserRobotMessageListAsserts.kt`: +```dart +extension MessageListAsserts on UserRobot { + Future assertMessage(String text, {bool isDisplayed = true}) async { + if (isDisplayed) { + expect($(MessageListPage.messageList.message.text).$(text), findsOneWidget); + } else { + expect($(text), findsNothing); + } + return this; + } + Future assertReaction(ReactionType type, {required bool isDisplayed}) async { ... } + Future assertMessageDeliveryStatus(MessageDeliveryStatus status, {int? count}) async { ... } +} +``` + +### 5.7 StreamTestCase base (`tests/stream_test_case.dart` β‰ˆ `StreamTestCase.kt`) +Provide a helper that owns the mock server + robots and the app boot, so each +test reads like the Android one. Implemented as a setup function used inside +`patrolTest`: +```dart +class StreamTestEnv { + late final MockServer mockServer; + late final BackendRobot backendRobot; + late final ParticipantRobot participantRobot; + late final UserRobot userRobot; + + Future setUp(PatrolTester $, {required String testName, InitActivity init = InitActivity.userLogin}) async { + mockServer = await MockServer.start(testName: testName); // fresh per-test server (own port) + backendRobot = BackendRobot(mockServer); + participantRobot = ParticipantRobot(mockServer); + userRobot = UserRobot($); + + // Option A seam: inject this test's mock URL into the global singleton + // BEFORE the app boots and calls connect(). + authController.debugConnectionOverride = StreamConnectionOverride( + baseURL: mockServer.httpUrl, + baseWsUrl: mockServer.wsUrl, + jwtFromMockServer: init is JwtActivity, // token provider β†’ /jwt/get + ); + + await $.pumpWidgetAndSettle(const StreamChatSampleApp()); + await grantAppPermissions($); // notifications, media + } + + // App-side reset (Patrol shares one process across bundled tests). + // debugReset() clears secure storage, disposes the client, and clears the + // connection override. + Future tearDown() async { + await authController.debugReset(); + await mockServer.stop(); + } +} +``` +`InitActivity` ports the Android `InitTestActivity` sealed class +(`UserLogin` / `Jwt(baseUrl)`). + +### 5.8 Tests (`tests/*.dart`) +Port the 11 Android suites. Use Patrol's `patrolTest` + an Allure-style `step()` +helper to keep the GIVEN/WHEN/THEN structure and Allure IDs. + +Android: +```kotlin +@AllureId("5675") +@Test fun test_addsReaction() { + step("GIVEN user opens the channel") { userRobot.login().openChannel() } + step("WHEN user sends the message") { userRobot.sendMessage(sampleText) } + step("AND user adds the reaction") { userRobot.addReaction(type = ReactionType.LIKE) } + step("THEN the reaction is added") { userRobot.assertReaction(ReactionType.LIKE, isDisplayed = true) } +} +``` +Dart (mimicked DSL): +```dart +patrolTest('test_addsReaction', tags: ['AllureId:5675'], ($) async { + final env = StreamTestEnv(); + await env.setUp($, testName: 'test_addsReaction'); + addTearDown(env.tearDown); + + await step('GIVEN user opens the channel', () async { await env.userRobot.login(); await env.userRobot.openChannel(); }); + await step('WHEN user sends the message', () async => env.userRobot.sendMessage(sampleText)); + await step('AND user adds the reaction', () async => env.userRobot.addReaction(ReactionType.like)); + await step('THEN the reaction is added', () async => env.userRobot.assertReaction(ReactionType.like, isDisplayed: true)); +}); +``` + +### 5.9 Retry + Allure (`e2e/support/`) +- **Retry:** Android's `RetryRule(3)` retries failures, clears DB, and captures + artifacts. Port as a wrapper around `patrolTest` (or use `flutter test`'s + `retry:` arg / a custom loop). On failure capture: screenshot + (`$.native.takeScreenshot` / `binding.takeScreenshot`), the widget tree dump, + and logs; attach to Allure. +- **Allure:** add `allure.properties`, emit results in the + `allure-results` format. `step(name, body)` records a step + timing; on + teardown write the `*-result.json`. Reuse the Fastlane `Allurefile` lanes + (`allure_upload`, `allure_launch`) unchanged β€” they're tool-agnostic. + +--- + +## 6. Production-code changes required in `sample_app` / `stream_chat_flutter` + +1. **Connection-override seam (Option A).** Add a nullable + `debugConnectionOverride` (a tiny `StreamConnectionOverride` holding + `baseURL`/`baseWsUrl`) to `AuthController`, and have + `auth_controller.dart::_buildStreamChatClient` read it (replacing the + hardcoded-commented lines ~54–55). Null in production β†’ SDK default base URL, + so the shipped diff is inert. The Patrol test sets + `authController.debugConnectionOverride` before `pumpWidget`. +2. **App-side reset for bundled tests.** Because Patrol shares one app process + and `authController` is a process-wide singleton that keeps `_client` alive + and auto-connects from `FlutterSecureStorage`, add a test-only reset path + used in `tearDown`: clear secure storage, `dispose()`/null the client, and + clear `debugConnectionOverride`. (Mirrors Android's per-test + `clearPackageData true`.) Server-side state is already fresh per test via the + driver β€” this only covers app-side leakage. +3. **Guard boot-path side effects under e2e.** When `debugConnectionOverride` is + set, skip/stub the side effects that assume a full native launch: + `NotificationService.initialize()` (in `app.dart` `initState`), push device + registration (`PushTokenManager`), and `FirebaseCrashlytics.recordError` (in + `_sampleAppLogHandler`). Otherwise pumping the root widget without Firebase + configured can throw. +4. **JWT via mock server (auth suite only).** When the e2e override indicates a + mock JWT flow, make the token provider fetch from `GET /jwt/get?platform=flutter`. +5. **Widget `Key`s / semantics identifiers.** Add `Stream_*` keys to the widgets + the page objects target (composer input/send, message cell/text, reactions, + channel list tile, avatar, headers, context-menu items, thread items, scroll + buttons, login button, JWT/connection screen). This touches + `stream_chat_flutter` components and some `sample_app` screens. +6. **Android cleartext for e2e build.** Add a debug `network_security_config` + permitting cleartext to `10.0.2.2` (mock server is HTTP/WS). +7. **iOS ATS exception** for `localhost` HTTP in the test build (Info.plist + `NSAppTransportSecurity` / `NSAllowsLocalNetworking`). + +--- + +## 7. Tooling: Gemfile + Fastlane + Patrol CLI + +### 7.1 Gemfile (root of repo or `sample_app/`) +Port the Android root `Gemfile`, dropping Sinatra deps (the mock server has its +own `Gemfile`): +```ruby +source 'https://rubygems.org' +gem 'fastlane', '2.225.0' +gem 'json' +gem 'rubocop', '1.38', group: :rubocop_dependencies +eval_gemfile('fastlane/Pluginfile') # include allure-testops plugin +``` +The mock server's own gems (`sinatra`, `puma`, `faye-websocket`, `eventmachine`, +`rackup`) come from cloning `stream-chat-test-mock-server` (its Gemfile). + +### 7.2 Fastlane lanes (port `Fastfile` + `Allurefile`) +New lanes under `sample_app/fastlane/` (or root), composing existing build lanes: +- `start_mock_server(local_server:, branch:)` β€” clone (or use local) + `stream-chat-test-mock-server`, `bundle install`, run `ruby driver.rb 4567 &`, + log to `logs/`. (Port from Android `start_mock_server`; Swift uses port 4566 β€” + pick one, e.g. **4567**, and keep it consistent in `MockServer._driverPort`.) +- `stop_mock_server` β€” `GET http://localhost:4567/stop`. +- `build_e2e_test` β€” `patrol build android` / `patrol build ios` (or + `flutter build` of the integration test target). Gate with the Android-style + `is_check_required` if desired. +- `run_e2e_test(batch:, batch_count:, device:, local_server:, mock_server_branch:)`: + 1. `start_mock_server` + 2. grant permissions / boot emulator-simulator (Patrol handles app perms; OS + setup via `adb`/`xcrun simctl` as on Android) + 3. `patrol test --target integration_test/tests/<...> [--dart-define ...]` + (Patrol drives both platforms; supports test sharding for batching) + 4. collect `allure-results` + 5. `stop_mock_server` +- `build_and_run_e2e_test` β€” convenience wrapper (port of Android lane). +- Reuse `Allurefile` lanes verbatim: `allure_launch`, `allure_upload`, + `allure_start_regression`. + +### 7.3 Patrol CLI +Add `patrol_cli` as a tool dependency; `patrol` + `integration_test` as +`dev_dependencies` in `sample_app/pubspec.yaml`. Create `patrol.yaml` with the +app id (`io.getstream.chat.android.flutter.sample` / `io.getstream.flutter`) and +test target config. + +--- + +## 8. CI (GitHub Actions) + +Add an `e2e` job (new workflow `e2e_test.yml`, or a job in +`stream_flutter_workflow.yml`), modeled on the Android pipeline: +- Matrix over `{android-emulator, ios-simulator}` and optional test batches. +- Steps: checkout β†’ setup Ruby + `bundle install` β†’ setup Flutter + `melos bootstrap` + β†’ boot emulator/simulator β†’ `bundle exec fastlane run_e2e_test` (which starts + the mock server, runs Patrol, gathers Allure results) β†’ `allure_upload`. +- Use `allure_launch` to create the TestOps launch and pass `LAUNCH_ID` between + jobs (as Android does). +- Keep it off the default PR path initially (manual / nightly / label-gated) to + avoid flakiness blocking PRs, matching how heavy e2e usually runs. + +--- + +## 9. melos wiring +- Add an `e2e` script group in `melos.yaml`, e.g. + `e2e:build`, `e2e:run` that `cd sample_app && patrol ...`, so it's invokable + like the existing `test:*` scripts. +- Ensure `sample_app` `integration_test/` doesn't get swept into the regular + `test:flutter` melos filter (it targets `dirExists: test`, so + `integration_test/` is naturally excluded β€” verify). + +--- + +## 10. Phased implementation + +1. **Foundations** + - Add Patrol + integration_test deps, `patrol.yaml`, `test_driver/`. + - Add the connection-options test seam in `sample_app` (Option A) and verify a + trivial Patrol test boots the app pointed at a manually-started mock server. +2. **Mock server client + robots** + - Port `MockServer`, `data_types.dart`, `BackendRobot`, `ParticipantRobot`. + - Validate against a locally running `driver.rb` (channels generate, participant + sends a message that appears in the app). +3. **Selectors** + - Add `Stream_*` `Key`s to widgets; build the page object files. +4. **DSL + base** + - Port the Patrol-ext helpers, `UserRobot` + assertion extensions, + `StreamTestEnv`/`StreamTestCase`, `step()`/Allure, retry harness. +5. **Tests** + - Port suites in order of value: `MessageListTests`, `ReactionsTests`, + `QuotedReplyTests`, `ChannelListTests`, then `Attachments`, `Giphy`, + `Drafts`, `DeliveryStatus`, `HyperLinks`, `Auth`, `Backend`. +6. **Tooling + CI** + - Gemfile, Fastlane lanes, Allure config, GitHub Actions job; get green on + both an Android emulator and an iOS simulator. + +--- + +## 11. Risks / open questions +- **Selector coverage** is the biggest effort: every Android `Stream_*` testTag + needs a Flutter `Key`/semantics equivalent; some widgets may need upstream + changes in `stream_chat_flutter`. +- **Async robots:** Dart's async HTTP means `BackendRobot`/`ParticipantRobot` + methods are `Future`s β€” slightly less terse than Kotlin's blocking chains. +- **Connectivity toggling** (offline/online for the auth/token tests) depends on + Patrol's native automator support per platform; verify wifi/airplane toggles + work on both iOS simulator and Android emulator (Android emulator can toggle; + iOS simulator network toggling is more limited β€” may need a host-level toggle + or to mock via the server's `freeze/delay` endpoints instead). +- **Driver port β€” one Flutter port, distinct from the native repos.** The port + is just the CLI arg to `driver.rb`, so we're free to choose. Flutter uses a + single port shared by both its platforms: + + | Suite | Host | Driver port | + |---|---|---| + | stream-chat-android (native) | `10.0.2.2` | 4567 *(existing)* | + | stream-chat-swift (native) | `localhost` | 4566 *(existing)* | + | **Flutter (iOS + Android)** | `10.0.2.2` / `localhost` | **4568** | + + - Distinct from 4566/4567 β†’ Flutter e2e never clashes with the native repos if + a developer runs them together. + - Flutter-iOS and Flutter-Android **share** 4568: in CI they're separate + matrix jobs on separate runners, so they never share a host. Note the + `10.0.2.2`/`localhost` split does *not* prevent a collision if both ran on + one host β€” both drivers bind the host's port β€” so for the rare local case of + running both platforms at once, override via `--dart-define=MOCK_DRIVER_PORT=...` + (and pass the matching port to `driver.rb` in that run). + - Keep `MockServer._driverPort` and the Fastlane lanes in sync on `4568`. +- **Cleartext/ATS:** must be enabled only for the e2e build flavor, not release. +- **JWT flow** for the auth suite requires the token provider to call the mock + server β€” confirm the sample app's auth path can be redirected in test mode. + +--- + +## 12. Implementation checklist + +Work top-to-bottom. Each phase ends in something runnable/verifiable so we never +go more than a step or two without feedback. Decision is **Option A** (Β§3). + +### Phase 0 β€” Spike: prove the seam end-to-end +- [x] Add a `Gemfile` (fastlane) + Fastlane skeleton, and port the + `start_mock_server` / `stop_mock_server` lanes from the native repos' + Fastfiles (clone-or-local mock-server repo β†’ `bundle install` β†’ launch + `ruby driver.rb 4568` in the background β†’ `/stop` on teardown). + β†’ Done in `sample_app/Fastfile` (shared, imported by both platform Fastfiles). + Verified end-to-end against the real driver on 4568: `/start/:test` spawns a + per-test server that answers `/ping` 200 after ~0.6s; `/stop` tears it down. +- [x] Add `patrol` + `integration_test` to `sample_app/pubspec.yaml` dev deps (and `patrol` to melos.yaml); `melos bootstrap`. β†’ patrol 4.6.1 resolved. `patrol_cli` is a global CLI (`dart pub global activate patrol_cli`, 4.4.0 used) β€” not a package dep. +- [x] Add Patrol config as a `patrol:` section **in `pubspec.yaml`** with `test_directory: integration_test` and app ids `io.getstream.chat.android.flutter.sample` / `io.getstream.flutter`. +- [x] Add `StreamConnectionOverride` + `debugConnectionOverride` to `AuthController`; wire into `_buildStreamChatClient` (Β§6.1). Plus an `isE2eTestRun` flag guarding Firebase/Crashlytics, push registration, and notifications (Β§6.3 pulled forward β€” required for in-process pumping). +- [x] Add platform host helper (`10.0.2.2` Android / `localhost` iOS). β†’ in `integration_test/_spike/mock_server.dart`. +- [x] Write one throwaway `patrolTest` that starts a mock server, sets the override, pumps `StreamChatSampleApp`, asserts the channel list loads. β†’ `integration_test/spike_seam_test.dart` (asserts `StreamChannelListHeader`). Analyzes clean. +- Connection config: **already satisfied** β€” iOS `Info.plist` has `NSAllowsArbitraryLoads=true`; Android **debug** manifest has `usesCleartextTraffic="true"` (Patrol uses a debug build). Β§6.6/Β§6.7 need no work. +- [x] **Native Patrol test-target integration.** + - Android: `PatrolJUnitRunner` as `testInstrumentationRunner` + `ANDROIDX_TEST_ORCHESTRATOR` + `androidx.test:orchestrator` (`android/app/build.gradle`); `MainActivityTest.java` entry class under `android/app/src/androidTest/`. + - iOS: `RunnerUITests` UI-testing target added to `Runner.xcodeproj` via the `xcodeproj` gem (script `ios/add_patrol_target.rb`), `RunnerUITests/RunnerUITests.m` bridge (`PATROL_INTEGRATION_TEST_IOS_RUNNER`), and the `target 'RunnerUITests'` block in the Podfile. +- [x] Run on an Android emulator. **βœ… PASS** β€” `patrol test` built, installed, ran the instrumentation, and the spike connected to the mock server and rendered the channel list. (Fix: placeholder token must be a structurally-valid JWT β€” reused qatest1's.) +- [x] Run on an iOS simulator. **βœ… PASS** β€” `TEST EXECUTE SUCCEEDED`, same spike. Two scripted-target gaps fixed (now baked into `ios/add_patrol_target.rb`): UI test bundle needs `GENERATE_INFOPLIST_FILE = YES` and `PRODUCT_NAME = $(TARGET_NAME)` (else the nameless `.xctest` collides as "Multiple commands produce …/PlugIns/.xctest"). +- **βœ… Phase 0 gate met: the Option A seam is proven on both Android emulator and iOS simulator.** Connection config (Β§6.6/Β§6.7) already satisfied β€” iOS `NSAllowsArbitraryLoads`, Android debug `usesCleartextTraffic`. +- ⚠️ **Toolchain note:** system Java is 26.0.1, which Gradle 8.13 rejects. Android builds need a supported JDK β€” set `JAVA_HOME=/opt/homebrew/opt/openjdk@21/...` (or `flutter config --jdk-dir`). Record this for CI. + +### Phase 1 β€” Foundations & app-side plumbing +- Directory layout (Β§4): not pre-created β€” folders form as files land in Phase 2+ (empty dirs aren't tracked by git and add nothing). +- [x] Implement `AuthController.debugReset()` (clear secure storage, dispose/null client, clear api key + push manager + connection override, back to `Unauthenticated`) for per-test teardown (Β§6.2). Wired into the spike's teardown. +- [x] Guard boot-path side effects under the e2e flag (`isE2eTestRun`): `NotificationService.initialize()`, `PushTokenManager`, `FirebaseCrashlytics.recordError` (Β§6.3). *(Done in Phase 0.)* +- [x] Android cleartext (Β§6.6) β€” already satisfied by the debug manifest's `usesCleartextTraffic="true"`. +- [x] iOS ATS (Β§6.7) β€” already satisfied by `NSAllowsArbitraryLoads`. +- [x] **Gate:** two tests in one bundle both pass on Android β€” each starts its own mock server on a fresh port and connects, with `debugReset()` wiping client/credentials/override in between. Proves per-test isolation (run 2 would fail on a leaked client/override otherwise). + +### Phase 2 β€” Mock server client + control robots +> Layout note: shared helpers live under `integration_test/` (`mock_server/`, +> `robots/`), not a sibling `e2e/` dir β€” Dart has no module boundary to justify +> the split, and Patrol only bundles `*_test.dart`, so non-test helpers there are +> safe. +- [x] Port `data_types.dart` (`AttachmentType`, `ReactionType`, `MessageDeliveryStatus`, `forbiddenWord`). +- [x] Implement `MockServer` (driver `/start/:test`, dynamic port, per-platform host, `get`/`post`, `stop` β†’ per-test server's `/stop`, `waitUntilReady`). +- [x] Port `BackendRobot` (generateChannels, fail/freeze messages, JWT revoke/invalidate/break). +- [x] Port `ParticipantRobot` (message/thread/edit/delete/quote, reactions, typing, read, giphy, attachments). +- [x] **Gate (scoped to robot layer):** `BackendRobot.generateChannels(...)` β†’ the connected app queries the mock server and renders a `StreamChannelListTile` (validated on Android, two runs). `ParticipantRobot` is a faithful verbatim port; asserting a participant message *renders* needs the channel open + message-cell selectors, so that assertion moves to Phase 4's message-list tests (validating it earlier via the channel-list preview proved brittle). + +### Phase 3 β€” Selectors +> Approach (revised): **reuse identifiers the SDK already exposes β€” exported +> widget types and existing keys β€” before adding anything to the source.** Add a +> `Stream_*` key only when no stable identifier exists, and only for a widget a +> landing suite actually needs (no speculative keys). Goal: as few source changes +> as possible. +- [x] Composer selectors with **zero SDK changes**: input field matched by its + exported type `StreamMessageComposerInputField`; send button by the key the + SDK already sets for its `AnimatedSwitcher` (`ValueKey('send_key')`). (An + earlier pass added `Stream_*` keys; reverted once existing identifiers + proved sufficient.) +- [x] PageObjects `pages/message_list_page.dart` (`composer.inputField` β†’ type, `composer.sendButton` β†’ `send_key`) and `pages/channel_list_page.dart` (channel tile β†’ `StreamChannelListTile` type). +- [x] **Gate:** test navigates channel list β†’ opens channel β†’ `enterText` into the composer field (by type) β†’ taps the send button (by existing key) β†’ the sent message renders. βœ… on Android (`integration_test/message_list_test.dart`). +- Remaining selectors resolved per-suite in Phase 4/5: existing type/key first; add a `Stream_*` key only where the SDK exposes nothing stable (e.g. one message cell among many). + +### Phase 4 β€” DSL, robots & base +> Lean scope: **no `patrol_ext` layer** β€” Patrol's `$` already provides +> `waitUntilVisible`/`tap`/`enterText`/`scrollTo`/`at` (the Android `uiautomator` +> layer only existed because UIAutomator was low-level). `retry` + Allure wiring +> deferred to Phase 6; `InitActivity`/jwt deferred to the auth suite (Phase 5). +- [x] `support/predefined_users.dart` (`UserCredentials` + `PredefinedUsers.qaTest1`) and `support/step.dart` (BDD `step()`; Allure hook point for Phase 5). +- [x] `UserRobot` (`login` via the real choose-user UI, `openChannel`, `sendMessage`) + `user_robot_message_list_asserts.dart` (`assertMessage`). More actions/asserts added per-suite in Phase 5. +- [x] `StreamTestEnv` (`support/stream_test_env.dart`) β€” `setUp` boots the app pointed at the mock server + owns the robots; `tearDown` = `debugReset` + `mockServer.stop`. +- [x] Removed the throwaway `spike_seam_test.dart` (the DSL test covers the flow; `debugReset` isolation runs in every teardown). +- [x] **Gate:** `message_list_test.dart` ported to the full DSL (`StreamTestEnv` + `UserRobot` + `step()`) passes on **both platforms** βœ… (Android + iOS). (Reactions test deferred to Phase 5 β€” needs reaction/context-menu selectors.) + +### Phase 5 β€” Tooling, Allure & CI +- [x] Fastlane lanes `run_e2e_test` / `build_e2e_test` / `build_and_run_e2e_test` (compose `start_mock_server`/`stop_mock_server`). `run_e2e_test` **validated locally on Android** end-to-end (start mock server β†’ `patrol test` β†’ stop). Fixed a real bug: the mock-server `bundle install`/driver must run under `Bundler.with_unbundled_env`, else the parent `bundle exec fastlane` context breaks it. +- [x] melos `e2e:run` script; confirmed `integration_test/` is excluded from `test:flutter` (`flutter test` only runs `test/`). +- [x] Single Flutter driver port `4568` used by `MockServer._driverPort` and the lanes (Β§11). +- [x] `sources_matrix` lane + `is_check_required` gating on `run_e2e_test`/`build_e2e_test` (iOS-style, via `fastlane-plugin-stream_actions`): the e2e lanes self-skip unless `sample_app`, `packages`, `melos.yaml`, or the e2e workflows changed (`@force_check` overrides). +- [x] GitHub Actions β€” two workflows, mirroring the native repos' split: + - `e2e_test.yml` β€” PR (label `e2e`) + manual `workflow_dispatch`. android + ios jobs; JDK 21; clones mock server; runs `run_e2e_test` + `allure_upload`; uploads logs. + - `e2e_test_cron.yml` β€” **separate nightly** (weeknights 01:00 UTC) + manual. Broader matrix (Android API 34/31/28; iOS 16/15), per-job `allure_launch` β†’ `run_e2e_test` β†’ `allure_upload` (shared launch via `ALLURE_LAUNCH_ID`) β†’ `allure_launch_removal` on cancel, plus a Slack-on-failure job. Guarded to the canonical repo. + - Both YAML-validated; not runnable here (no GH runner). Scheduled workflows only fire once on the default branch. +- [x] **Allure results producer β€” built & validated on both platforms.** A Dart reporter (`integration_test/allure/allure.dart`) hooked into `step()` and a `streamTest()` wrapper produces standard Allure 2 result JSON (uuid, historyId, name, fullName, status, start/stop, nested steps). Captures `passed`/`failed`/`broken`. Since on-device files don't survive Android scoped-storage / `clearPackageData`, and Dart `print` doesn't reach patrol's stdout, the reporter emits **chunked `ALLURE-RESULT::` markers** (sub-1024 to dodge log truncation); `collect_allure_results` reassembles them from the device log β€” `adb logcat` (Android) / `xcrun simctl … log show` (iOS) β€” into `allure-results/`. Validated end-to-end via `run_e2e_test` on **Android and iOS**: green test β†’ `allure-results/-result.json` with the 3 GIVEN/WHEN/THEN steps; a failing run produced a `broken` result. +- Dropped the Android test orchestrator + `clearPackageData` from `build.gradle` (added in Phase 3) β€” they wiped the app's storage and aren't needed (isolation is in-process via `debugReset`). +- [x] **Allure upload β€” built & validated against live TestOps.** `allure_upload` / `allure_launch` / `install_allurectl` lanes drive `allurectl` (endpoint `streamio.testops.cloud`, project `135`, `ALLURE_TOKEN` from env). Verified real uploads (launches 276343 Android-results, 276345 iOS-results). Wired into CI (`allure_upload` step on both jobs, `if: always()`, `ALLURE_TOKEN` from `secrets`). +- **Gate:** e2e suite runs green on both platforms via `run_e2e_test`, produces valid Allure results on both, and uploads to TestOps β€” all validated locally. Only the GitHub-Actions run itself is unverified here (needs `ALLURE_TOKEN` added as a repo secret + a workflow trigger). + +### Phase 6 β€” Port the test suites +Port in value order; each is a checkpoint: +- [ ] `MessageListTests` +- [ ] `ReactionsTests` +- [ ] `QuotedReplyTests` +- [ ] `ChannelListTests` +- [ ] `AttachmentsTests` +- [ ] `GiphyTests` +- [ ] `DraftMessagesTests` +- [ ] `MessageDeliveryStatusTests` +- [ ] `HyperLinksTests` +- [ ] `AuthTests` (needs JWT-from-mock-server + connectivity toggling) +- [ ] `BackendTests` +- [ ] **Gate:** full suite green locally on Android emulator + iOS simulator. \ No newline at end of file diff --git a/sample_app/integration_test/allure/allure.dart b/sample_app/integration_test/allure/allure.dart new file mode 100644 index 0000000000..beeb8988f2 --- /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..deb8261d77 --- /dev/null +++ b/sample_app/integration_test/message_list_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'robots/user_robot_message_list_asserts.dart'; +import 'support/step.dart'; +import 'support/stream_test_case.dart'; +import 'support/stream_test_env.dart'; + +void main() { + const sampleText = 'Test'; + + streamTest('message list updates when the user sends a message', ($) async { + final env = StreamTestEnv(); + await env.setUp($); + addTearDown(env.tearDown); + + await step('GIVEN the user opens a channel', () async { + await env.backendRobot.generateChannels(channelsCount: 1); + await env.userRobot.login(); + await env.userRobot.openChannel(); + }); + + await step('WHEN the user sends a message', () async { + await env.userRobot.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..bfd360c056 --- /dev/null +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -0,0 +1,89 @@ +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' : 'localhost'; + + static const _driverPort = + String.fromEnvironment('MOCK_DRIVER_PORT', defaultValue: '4568'); + + static const _httpTimeout = Duration(seconds: 10); + + static Future start({String? testName}) async { + final name = testName ?? _currentTestName(); + final driverUrl = 'http://$_host:$_driverPort'; + final port = (await _get('$driverUrl/start/$name')).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: 15), + }) async { + final deadline = DateTime.now().add(timeout); + while (DateTime.now().isBefore(deadline)) { + final ready = await _statusCode('$url/ping') + .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) async { + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + return res.transform(utf8.decoder).join().timeout(_httpTimeout); + } finally { + client.close(force: true); + } + } + + static Future _statusCode(String url) async { + final client = HttpClient()..connectionTimeout = _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..14ed7fa3e2 --- /dev/null +++ b/sample_app/integration_test/robots/backend_robot.dart @@ -0,0 +1,56 @@ +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..8fcc785fe1 --- /dev/null +++ b/sample_app/integration_test/robots/user_robot.dart @@ -0,0 +1,33 @@ +import 'package:patrol/patrol.dart'; + +import '../pages/channel_list_page.dart'; +import '../pages/message_list_page.dart'; +import '../support/predefined_users.dart'; + +class UserRobot { + UserRobot(this.$); + + final PatrolIntegrationTester $; + + Future login([ + UserCredentials user = PredefinedUsers.currentUser, + ]) async { + final entry = $(user.name); + await entry.scrollTo(); + await entry.tap(); + return this; + } + + Future openChannel({int index = 0}) async { + final tile = $(ChannelListPage.channelTile).at(index); + await tile.waitUntilVisible(); + await tile.tap(); + return this; + } + + Future sendMessage(String text) async { + await $(MessageListPage.composer.inputField).enterText(text); + await $(MessageListPage.composer.sendButton).tap(); + 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..0b0fa8fdef --- /dev/null +++ b/sample_app/integration_test/robots/user_robot_message_list_asserts.dart @@ -0,0 +1,8 @@ +import 'user_robot.dart'; + +extension UserRobotMessageListAsserts on UserRobot { + Future assertMessage(String text) async { + await $(text).waitUntilVisible(); + 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..d37db502cb --- /dev/null +++ b/sample_app/integration_test/support/step.dart @@ -0,0 +1,4 @@ +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..99c4c3cad0 --- /dev/null +++ b/sample_app/integration_test/support/stream_test_case.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart' show Invoker; + +import '../allure/allure.dart'; + +void streamTest( + String description, + Future Function(PatrolIntegrationTester $) callback, { + String? allureId, +}) { + patrolTest(description, ($) async { + Allure.instance.startTest( + name: description, + fullName: Invoker.current?.liveTest.test.name ?? description, + labels: {if (allureId != null) 'AS_ID': allureId}, + ); + try { + await callback($); + 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; + } + }); +} 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..643718900e --- /dev/null +++ b/sample_app/integration_test/support/stream_test_env.dart @@ -0,0 +1,37 @@ +import 'package:patrol/patrol.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 { + late final MockServer mockServer; + late final BackendRobot backendRobot; + late final ParticipantRobot participantRobot; + late final UserRobot userRobot; + + Future setUp(PatrolIntegrationTester $) async { + mockServer = await MockServer.start(); + backendRobot = BackendRobot(mockServer); + participantRobot = ParticipantRobot(mockServer); + userRobot = UserRobot($); + + authController.debugConnectionOverride = StreamConnectionOverride( + baseURL: mockServer.url, + baseWsUrl: mockServer.wsUrl, + ); + + await $.pumpWidgetAndSettle(const StreamChatSampleApp()); + } + + Future tearDown() async { + try { + await authController.debugReset(); + } finally { + await mockServer.stop(); + } + } +} diff --git a/sample_app/ios/Gemfile.lock b/sample_app/ios/Gemfile.lock new file mode 100644 index 0000000000..ad8148083b --- /dev/null +++ b/sample_app/ios/Gemfile.lock @@ -0,0 +1,329 @@ +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) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1253.0) + aws-sdk-core (3.249.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.128.0) + aws-sdk-core (~> 3, >= 3.248.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.224.0) + aws-sdk-core (~> 3, >= 3.248.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.16.2) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.16.2) + 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) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) + 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.6) + 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) + escape (0.0.4) + ethon (0.18.0) + ffi (>= 1.15.0) + logger + excon (0.112.0) + faraday (1.10.5) + 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.235.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.8, < 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, < 1.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.1.0, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + 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-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.3) + xctest_list (= 1.2.1) + fastlane-sirp (1.1.0) + ffi (1.17.4-arm64-darwin) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.101.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.19.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-iamcredentials_v1 (0.27.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.62.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.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.6.0) + google-cloud-storage (1.60.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.16.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + 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.14.8) + concurrent-ruby (~> 1.0) + jmespath (1.6.2) + json (2.19.5) + jwt (3.2.0) + base64 + logger (1.7.0) + 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) + mutex_m (0.3.0) + nanaimo (0.4.0) + nap (1.1.0) + naturally (2.3.0) + netrc (0.11.0) + nkf (0.2.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + public_suffix (4.0.7) + 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 (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.4.1) + securerandom (0.4.1) + security (0.1.5) + signet (0.21.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + 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) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + 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 + arm64-darwin + +DEPENDENCIES + cocoapods + fastlane + fastlane-plugin-firebase_app_distribution + fastlane-plugin-stream_actions + multi_json + +BUNDLED WITH + 2.6.8 diff --git a/sample_app/ios/Podfile b/sample_app/ios/Podfile index f17bddc919..0710354985 100644 --- a/sample_app/ios/Podfile +++ b/sample_app/ios/Podfile @@ -31,6 +31,11 @@ target 'Runner' do use_frameworks! use_modular_headers! + # Patrol e2e UI test target. + target 'RunnerUITests' do + inherit! :complete + end + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 061be6260d..454a0220c2 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -11,13 +11,27 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 8210C58B8A5DAB805ACF46A1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 984969FB0FCC70C82B1D01CC /* Pods_Runner_RunnerUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2979FD476C8342D4BE16EDE3 /* Pods_Runner_RunnerUITests.framework */; }; + C313D84D1A521524E97C5A08 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 845BFD3A8BBE590E7BAE83AE /* RunnerUITests.m */; }; C3E4B5F67890A1B2C3D4E5F7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E4B5F67890A1B2C3D4E5F6 /* SceneDelegate.swift */; }; + F06BB2D34B124C927609E541 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6B213AE2D543B7BADA38E33 /* Foundation.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + EC56DE3EBD0AD91F86EB6C86 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 0BC14C55242B5A7A0028DE94 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; @@ -45,13 +59,19 @@ 0BC14C5B242B5FF50028DE94 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2979FD476C8342D4BE16EDE3 /* Pods_Runner_RunnerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner_RunnerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3F56E62AADA13256B99B3FD0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 484CBB9AEE4F8FC97D0F6A54 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F71825255CA4DD327F745DE /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 62748C3D21C600131729B6A7 /* Pods-Runner-RunnerUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.profile.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 845BFD3A8BBE590E7BAE83AE /* RunnerUITests.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RunnerUITests.m; path = RunnerUITests/RunnerUITests.m; sourceTree = ""; }; + 87DD5B8A5D8FFF81FBE78F7A /* Pods-Runner-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.release.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.release.xcconfig"; sourceTree = ""; }; 90B43020AC538F68084BBAFD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -60,8 +80,10 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A6B213AE2D543B7BADA38E33 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; C3E4B5F67890A1B2C3D4E5F6 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + F1BB505AA8ADA090AF0076DE /* Pods-Runner-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.debug.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -69,17 +91,45 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 8210C58B8A5DAB805ACF46A1 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + C1289C2FFEC46190EEF8C3CE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F06BB2D34B124C927609E541 /* Foundation.framework in Frameworks */, + 984969FB0FCC70C82B1D01CC /* Pods_Runner_RunnerUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7926AAA8A8EB00DB5D149360 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 845BFD3A8BBE590E7BAE83AE /* RunnerUITests.m */, + ); + name = RunnerUITests; + sourceTree = SOURCE_ROOT; + }; + 848E793A104447E77BE01B37 /* iOS */ = { + isa = PBXGroup; + children = ( + A6B213AE2D543B7BADA38E33 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; 9278E7173A28D18D316BCCAC /* Frameworks */ = { isa = PBXGroup; children = ( 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */, + 848E793A104447E77BE01B37 /* iOS */, + 2979FD476C8342D4BE16EDE3 /* Pods_Runner_RunnerUITests.framework */, ); name = Frameworks; sourceTree = ""; @@ -87,6 +137,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -104,6 +155,7 @@ CF168B61BAB91958681C7C21 /* Pods */, DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */, 9278E7173A28D18D316BCCAC /* Frameworks */, + 7926AAA8A8EB00DB5D149360 /* RunnerUITests */, ); sourceTree = ""; }; @@ -111,6 +163,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 5F71825255CA4DD327F745DE /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -146,6 +199,9 @@ 3F56E62AADA13256B99B3FD0 /* Pods-Runner.debug.xcconfig */, 484CBB9AEE4F8FC97D0F6A54 /* Pods-Runner.release.xcconfig */, 90B43020AC538F68084BBAFD /* Pods-Runner.profile.xcconfig */, + 87DD5B8A5D8FFF81FBE78F7A /* Pods-Runner-RunnerUITests.release.xcconfig */, + F1BB505AA8ADA090AF0076DE /* Pods-Runner-RunnerUITests.debug.xcconfig */, + 62748C3D21C600131729B6A7 /* Pods-Runner-RunnerUITests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -153,6 +209,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 29BAFE1A25301A6CE2A3BA97 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8A84C2230750F0B728CA3023 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 3921E106080881E83E998BFB /* [CP] Check Pods Manifest.lock */, + 08E8A4ECA2C1C2F244E229AF /* Sources */, + C1289C2FFEC46190EEF8C3CE /* Frameworks */, + 3DCFC2F9E1881E1CDF31C6A9 /* Resources */, + BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 7BBDFC82F80F89255BBCEC2F /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 5F71825255CA4DD327F745DE /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -166,13 +242,15 @@ 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 0BC14C55242B5A7A0028DE94 /* Embed App Extensions */, 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */, - 78D972C66518C545847BBB5A /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -202,16 +280,27 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 29BAFE1A25301A6CE2A3BA97 /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 3DCFC2F9E1881E1CDF31C6A9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -227,39 +316,43 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 3921E106080881E83E998BFB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-RunnerUITests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 78D972C66518C545847BBB5A /* [CP] Copy Pods Resources */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/firebase_messaging/firebase_messaging_Privacy.bundle", + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "[CP] Copy Pods Resources"; + name = "Thin Binary"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/firebase_messaging_Privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -268,95 +361,23 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", - "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreExtension/FirebaseCoreExtension.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCrashlytics/FirebaseCrashlytics.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseRemoteConfigInterop/FirebaseRemoteConfigInterop.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseSessions/FirebaseSessions.framework", - "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", - "${BUILT_PRODUCTS_DIR}/PromisesSwift/Promises.framework", - "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", - "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", - "${BUILT_PRODUCTS_DIR}/audio_session/audio_session.framework", - "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", - "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", - "${BUILT_PRODUCTS_DIR}/file_selector_ios/file_selector_ios.framework", + "${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", - "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", - "${BUILT_PRODUCTS_DIR}/gal/gal.framework", - "${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework", "${BUILT_PRODUCTS_DIR}/get_thumbnail_video/get_thumbnail_video.framework", - "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", - "${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", - "${BUILT_PRODUCTS_DIR}/record_ios/record_ios.framework", - "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", - "${BUILT_PRODUCTS_DIR}/sqflite_darwin/sqflite_darwin.framework", - "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", - "${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework", - "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", - "${BUILT_PRODUCTS_DIR}/video_player_avfoundation/video_player_avfoundation.framework", - "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework", + "${BUILT_PRODUCTS_DIR}/patrol/patrol.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreExtension.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCrashlytics.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseRemoteConfigInterop.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseSessions.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Promises.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_session.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaAsyncSocket.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/gal.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/get_thumbnail_video.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/record_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite_darwin.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_player_avfoundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/patrol.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -378,6 +399,36 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework", + "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/get_thumbnail_video/get_thumbnail_video.framework", + "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", + "${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework", + "${BUILT_PRODUCTS_DIR}/patrol/patrol.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaAsyncSocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/get_thumbnail_video.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/patrol.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; D9B8FF135C3B420557191353 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -403,6 +454,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 08E8A4ECA2C1C2F244E229AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C313D84D1A521524E97C5A08 /* RunnerUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -415,6 +474,15 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 7BBDFC82F80F89255BBCEC2F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Runner; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = EC56DE3EBD0AD91F86EB6C86 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -435,6 +503,25 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 193194185D13B875BF2C7E81 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 62748C3D21C600131729B6A7 /* Pods-Runner-RunnerUITests.profile.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { @@ -519,6 +606,43 @@ }; name = Profile; }; + 77FD324475B8A26B07568B97 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 87DD5B8A5D8FFF81FBE78F7A /* Pods-Runner-RunnerUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 88F210A499A992DE5779D089 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F1BB505AA8ADA090AF0076DE /* Pods-Runner-RunnerUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_STYLE = Automatic; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -695,6 +819,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 8A84C2230750F0B728CA3023 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77FD324475B8A26B07568B97 /* Release */, + 88F210A499A992DE5779D089 /* Debug */, + 193194185D13B875BF2C7E81 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -716,6 +850,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..521c8a269d --- /dev/null +++ b/sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,184 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", + "version" : "1.2024072200.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "csqlite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simolus3/CSQLite.git", + "state" : { + "revision" : "1ee46d19a4f451a7aa64ffc64fc99b4748131e62" + } + }, + { + "identity" : "dkcamera", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKCamera", + "state" : { + "branch" : "master", + "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" + } + }, + { + "identity" : "dkimagepickercontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKImagePickerController", + "state" : { + "branch" : "4.3.9", + "revision" : "0bdfeacefa308545adde07bef86e349186335915" + } + }, + { + "identity" : "dkphotogallery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKPhotoGallery", + "state" : { + "branch" : "master", + "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", + "version" : "12.14.0" + } + }, + { + "identity" : "google-ads-on-device-conversion-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", + "state" : { + "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", + "version" : "3.6.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", + "version" : "12.14.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6", + "version" : "1.69.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a", + "version" : "5.3.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", + "version" : "101.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "3851d94a41890dea16dc3db34caf60e585cb4163", + "version" : "2.30910.1" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "f4a19a3c313dc2616c70bb49d29a799fb16be837", + "version" : "2.4.1" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage", + "state" : { + "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", + "version" : "5.21.7" + } + }, + { + "identity" : "swiftygif", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kirualex/SwiftyGif.git", + "state" : { + "revision" : "4430cbc148baa3907651d40562d96325426f409a", + "version" : "5.4.5" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController", + "state" : { + "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e", + "version" : "2.8.0" + } + } + ], + "version" : 2 +} diff --git a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5db441f58a..d77590b84e 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"> @@ -56,6 +56,16 @@ + + + + void initState() { super.initState(); _notificationService.onNotificationTap = _onNotificationTap; - _notificationService.initialize(); + // Notifications rely on Firebase, which isn't initialized under e2e tests. + 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..ea9af72db1 100644 --- a/sample_app/lib/auth/auth_controller.dart +++ b/sample_app/lib/auth/auth_controller.dart @@ -26,9 +26,17 @@ final _chatPersistenceClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, ); +/// True when running under e2e tests, detected via the presence of a +/// [StreamConnectionOverride]. Used to skip boot-path side effects that assume +/// a full native launch (Firebase, push registration, notifications). +bool get isE2eTestRun => authController.debugConnectionOverride != null; + Future _sampleAppLogHandler(LogRecord record) async { if (kDebugMode) StreamChatClient.defaultLogHandler(record); + // Crashlytics isn't initialized under e2e tests (the app is pumped in-process). + if (isE2eTestRun) return; + // report errors to Firebase Crashlytics if (record.error != null || record.stackTrace != null) { await FirebaseCrashlytics.instance.recordError( @@ -39,7 +47,27 @@ Future _sampleAppLogHandler(LogRecord record) async { } } -StreamChatClient _buildStreamChatClient(String apiKey) { +/// Test-only override that points the [StreamChatClient] at a mock server. +/// +/// `null` in production, so the SDK uses its default base URL. E2E tests set +/// [AuthController.debugConnectionOverride] before the first [AuthController.connect] +/// so the client talks to the local mock server instead of the real backend. +@visibleForTesting +class StreamConnectionOverride { + /// Creates an override; pass `null` for any field to keep the SDK default. + const StreamConnectionOverride({this.baseURL, this.baseWsUrl}); + + /// REST base URL, e.g. `http://10.0.2.2:` (Android) / `http://localhost:` (iOS). + final String? baseURL; + + /// WebSocket base URL, e.g. `ws://10.0.2.2:` / `ws://localhost:`. + final String? baseWsUrl; +} + +StreamChatClient _buildStreamChatClient( + String apiKey, { + StreamConnectionOverride? connectionOverride, +}) { const logLevel = kDebugMode ? Level.INFO : Level.SEVERE; return StreamChatClient( apiKey, @@ -51,8 +79,9 @@ StreamChatClient _buildStreamChatClient(String apiKey) { return error is StreamChatNetworkError && error.isRetriable; }, ), - //baseURL: 'http://:3030', - //baseWsUrl: 'ws://:8800', + // Null in production β†’ SDK defaults; set by e2e tests to hit the mock server. + baseURL: connectionOverride?.baseURL, + baseWsUrl: connectionOverride?.baseWsUrl, )..chatPersistenceClient = _chatPersistenceClient; } @@ -93,6 +122,14 @@ class AuthController extends ValueNotifier { /// The active client, or `null` before the first [connect]. StreamChatClient? get client => _client; + /// Test-only connection override (see [StreamConnectionOverride]). + /// + /// Set this before the first [connect] in e2e tests to point the client at + /// the local mock server; leave `null` in production. Read once when the + /// client is built, so set it before the app calls [connect]. + @visibleForTesting + StreamConnectionOverride? debugConnectionOverride; + String? _activeApiKey; PushTokenManager? _pushTokenManager; @@ -142,7 +179,10 @@ class AuthController extends ValueNotifier { _client = null; } - final client = _client ??= _buildStreamChatClient(apiKey); + final client = _client ??= _buildStreamChatClient( + apiKey, + connectionOverride: debugConnectionOverride, + ); _activeApiKey = apiKey; try { @@ -157,11 +197,14 @@ class AuthController extends ValueNotifier { ]); } - _pushTokenManager = PushTokenManager( - client: client, - iosPushProvider: _kIosPushProvider, - androidPushProvider: _kAndroidPushProvider, - )..registerDevice(); + // Push relies on Firebase, which isn't initialized under e2e tests. + if (!isE2eTestRun) { + _pushTokenManager = PushTokenManager( + client: client, + iosPushProvider: _kIosPushProvider, + androidPushProvider: _kAndroidPushProvider, + )..registerDevice(); + } value = Authenticated(ownUser); } catch (_) { @@ -194,6 +237,30 @@ class AuthController extends ValueNotifier { client.disconnectUser(flushChatPersistence: flushPersistence).ignore(); } + /// Resets all session state so the next e2e test starts clean. + /// + /// Patrol runs every test in one app process against this process-wide + /// singleton, so credentials, the client, and the connection override would + /// otherwise leak between tests. Unlike [dispose] this keeps the notifier + /// usable. Call from test teardown. + @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 a5f7b789b7..b569d17def 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -50,9 +50,22 @@ dependencies: dev_dependencies: flutter_launcher_icons: ^0.14.4 + integration_test: + sdk: flutter + patrol: ^4.6.1 flutter: uses-material-design: true assets: - assets/ - pubspec.lock + +# Patrol e2e configuration (config lives in pubspec.yaml, not a separate file). +# Tests live in integration_test/ (Patrol defaults to patrol_test/ otherwise). +patrol: + app_name: Stream Chat + test_directory: integration_test + android: + package_name: io.getstream.chat.android.flutter.sample + ios: + bundle_id: io.getstream.flutter diff --git a/sample_app/tool/boot_ios_simulator.sh b/sample_app/tool/boot_ios_simulator.sh new file mode 100755 index 0000000000..e9697fe5d0 --- /dev/null +++ b/sample_app/tool/boot_ios_simulator.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Boots an iOS simulator for Patrol e2e on CI. +# +# Builds use Xcode 26.2 on macos-15; only the simulator runtime varies (see +# stream-chat-swift cron-checks). For downloaded runtimes, pass the simctl name +# only: boot_ios_simulator.sh custom-test-device +set -euo pipefail + +DEVICE_NAME="${1:?usage: boot_ios_simulator.sh [ios-version]}" +IOS_VERSION="${2:-${IOS_SIMULATOR_VERSION:-}}" + +if [[ -z "${GITHUB_ENV:-}" ]]; then + echo "GITHUB_ENV must be set (CI step env)" >&2 + exit 1 +fi + +xcrun simctl shutdown all 2>/dev/null || true + +device_id="$( + xcrun simctl list devices available --json | python3 -c ' +import json +import sys + +target, ios_version = sys.argv[1], sys.argv[2] +data = json.load(sys.stdin) + +if ios_version: + runtime_slug = "iOS-" + ios_version.replace(".", "-") + for runtime, devices in data["devices"].items(): + if runtime_slug not in runtime: + continue + for device in devices: + if device["name"] == target and device.get("isAvailable", True): + print(device["udid"]) + sys.exit(0) +else: + for runtime, devices in data["devices"].items(): + if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"): + continue + for device in devices: + if device["name"] == target and device.get("isAvailable", True): + print(device["udid"]) + sys.exit(0) + +matches = [] +for runtime, devices in data["devices"].items(): + if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"): + continue + for device in devices: + if device.get("isAvailable", True): + name = device["name"] + os_tag = runtime.split("iOS-")[-1] + matches.append(f"{name} ({os_tag})") + +hint = "\n".join(sorted(matches)[:20]) if matches else "(none)" +version_hint = f" on iOS {ios_version}" if ios_version else "" +raise SystemExit( + f"No available simulator {target!r}{version_hint}\n" + f"Available iOS simulators:\n{hint}" +) +' "$DEVICE_NAME" "$IOS_VERSION" +)" + +echo "device_id=${device_id}" >>"$GITHUB_ENV" +xcrun simctl boot "${device_id}" 2>/dev/null || true +xcrun simctl bootstatus "${device_id}" -b + +# https://github.com/leancodepl/patrol/issues/1282 +sleep 10 + +if [[ -n "$IOS_VERSION" ]]; then + echo "Booted ${DEVICE_NAME} (${device_id}) on iOS ${IOS_VERSION}" +else + echo "Booted ${DEVICE_NAME} (${device_id})" +fi diff --git a/sample_app/tool/install_ios_runtime.sh b/sample_app/tool/install_ios_runtime.sh new file mode 100755 index 0000000000..3bad4221b6 --- /dev/null +++ b/sample_app/tool/install_ios_runtime.sh @@ -0,0 +1,59 @@ +#!/bin/bash -e +# Installs a downloaded iOS simulator runtime image (DMG). +# Ported from stream-chat-swift/Scripts/install_ios_runtime.sh + +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|&2 + echo "Run: dart pub global activate patrol_cli" >&2 + exit 1 +fi + +if grep -q 'combinedInvocation' "${TARGET}"; then + echo "patrol_cli Android Gradle patch already applied" +else +python3 - "${TARGET}" <<'PY' +import sys + +path = sys.argv[1] +text = open(path, encoding="utf-8").read() + +old = """ // :app:assembleDebug + + process = + await _processManager.start( + options.toGradleAssembleInvocation( + isWindows: _platform.isWindows, + ), + runInShell: true, + workingDirectory: _rootDirectory.childDirectory('android').path, + environment: switch (javaPath) { + final String javaPath => {'JAVA_HOME': javaPath}, + _ => {}, + }, + ) + ..disposedBy(scope); + process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); + process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); + exitCode = await process.exitCode; + if (exitCode == exitCodeInterrupted) { + const cause = 'Gradle build interrupted'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + } else if (exitCode != 0) { + final cause = 'Gradle build failed with code $exitCode'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + } + + // :app:assembleDebugAndroidTest + + process = + await _processManager.start( + options.toGradleAssembleTestInvocation( + isWindows: _platform.isWindows, + ), + runInShell: true, + workingDirectory: _rootDirectory.childDirectory('android').path, + environment: switch (javaPath) { + final String javaPath => {'JAVA_HOME': javaPath}, + _ => {}, + }, + ) + ..disposedBy(scope); + process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); + process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); + + exitCode = await process.exitCode; + if (exitCode == 0) { + task.complete('Completed building $subject'); + } else if (exitCode == exitCodeInterrupted) { + const cause = 'Gradle build interrupted'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + } else { + final cause = 'Gradle build failed with code $exitCode'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + }""" + +new = """ // :app:assembleDebug + :app:assembleDebugAndroidTest in one invocation + // so Flutter registers compileFlutterBuildDebug (Flutter 3.44+). + // Patched by sample_app/tool/patch_patrol_cli_android_gradle.sh + + final assembleInvocation = options.toGradleAssembleInvocation( + isWindows: _platform.isWindows, + ); + final testInvocation = options.toGradleAssembleTestInvocation( + isWindows: _platform.isWindows, + ); + final combinedInvocation = [ + testInvocation.first, + assembleInvocation[1], + testInvocation[1], + ...testInvocation.skip(2), + ]; + + process = + await _processManager.start( + combinedInvocation, + runInShell: true, + workingDirectory: _rootDirectory.childDirectory('android').path, + environment: switch (javaPath) { + final String javaPath => {'JAVA_HOME': javaPath}, + _ => {}, + }, + ) + ..disposedBy(scope); + process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); + process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); + + exitCode = await process.exitCode; + if (exitCode == 0) { + task.complete('Completed building $subject'); + } else if (exitCode == exitCodeInterrupted) { + const cause = 'Gradle build interrupted'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + } else { + final cause = 'Gradle build failed with code $exitCode'; + task.fail('Failed to build $subject ($cause)'); + throw Exception(cause); + }""" + +if old not in text: + print("patrol_cli android_test_backend.dart changed; patch no longer applies", file=sys.stderr) + sys.exit(1) + +open(path, "w", encoding="utf-8").write(text.replace(old, new, 1)) +print(f"Patched {path}") +PY +fi + +# Drop the precompiled snapshot so `patrol` runs from the patched sources. +rm -f "${PUB_CACHE}/global_packages/patrol_cli/bin/"*.snapshot 2>/dev/null || true From f6943f1210144cc6dadf92769aa4021150f69477 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 10:19:03 +0100 Subject: [PATCH 02/33] Remove useless comments --- .github/actions/setup-ios-runtime/action.yml | 2 +- .github/workflows/e2e_test.yml | 3 --- .github/workflows/e2e_test_cron.yml | 3 +-- .gitignore | 4 --- melos.yaml | 2 -- sample_app/Allurefile | 9 ------- sample_app/Fastfile | 17 ------------ sample_app/android/app/build.gradle | 1 - .../flutter/sample/MainActivityTest.java | 2 -- sample_app/ios/Podfile | 1 - sample_app/ios/add_patrol_target.rb | 7 ----- sample_app/lib/app.dart | 1 - sample_app/lib/auth/auth_controller.dart | 26 ------------------- sample_app/pubspec.yaml | 2 -- sample_app/tool/boot_ios_simulator.sh | 6 ----- sample_app/tool/install_ios_runtime.sh | 2 -- .../tool/patch_patrol_cli_android_gradle.sh | 12 +-------- 17 files changed, 3 insertions(+), 97 deletions(-) diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 274b35382d..9493dcb741 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -1,5 +1,5 @@ name: Setup iOS Runtime -description: Download and install a requested iOS simulator runtime (stream-chat-swift parity) +description: Download and install a requested iOS simulator runtime inputs: version: description: iOS runtime version (e.g. 17.5) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 62be1c8ac5..80880ee03f 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -16,7 +16,6 @@ concurrency: env: flutter_version: "3.x" - # Must match setup-xcode and `patrol test --ios` (Patrol e2e on macos-26). ios_simulator_version: "26.2" ios_simulator_device: "iPhone 17" @@ -82,8 +81,6 @@ jobs: steps: - uses: actions/checkout@v6 - # Align Xcode with the iOS simulator runtime (device_info_plus 12.4+ and - # Patrol both need Xcode 26.x; terminate-app flakes without this match). - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.ios_simulator_version }} diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 79157983fb..908637d302 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -2,7 +2,7 @@ name: E2E Tests Nightly on: schedule: - - cron: "0 1 * * 1-5" # weeknights at 01:00 UTC + - cron: "0 1 * * 1-5" workflow_dispatch: inputs: mock_server_branch: @@ -124,7 +124,6 @@ jobs: steps: - uses: actions/checkout@v6 - # Build with Xcode 26.2 on every matrix row; only the simulator OS varies. - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: "26.2" diff --git a/.gitignore b/.gitignore index 7df70606ff..7423d30d35 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,9 @@ *.class *.log *.pyc -# E2E: mock-server checkout cloned by the `start_mock_server` Fastlane lane /sample_app/stream-chat-test-mock-server/ -# E2E: Patrol-generated test bundle (regenerated on every `patrol test`) /sample_app/integration_test/test_bundle.dart -# E2E: Allure results assembled from test runs /sample_app/allure-results/ -# E2E: allurectl binary downloaded by the Fastlane allure lanes /sample_app/allurectl *.swp .DS_Store diff --git a/melos.yaml b/melos.yaml index 546ba722de..ce7e8581c6 100644 --- a/melos.yaml +++ b/melos.yaml @@ -205,8 +205,6 @@ scripts: # `docs_*` are excluded β€” their goldens commit only the platform/macOS # variant and would fail on Linux CI runners. They're regenerated by # `update:goldens:docs` on a dedicated macOS workflow instead. - # Note: `flutter test` runs `test/` only, so `integration_test/` (the e2e - # suite, run via Patrol) is never picked up here. run: melos exec -c 4 --fail-fast --ignore="docs_*" -- "flutter test --coverage" description: Run Flutter tests for a specific package in this project. packageFilters: diff --git a/sample_app/Allurefile b/sample_app/Allurefile index ae366dccec..bef07dfc13 100644 --- a/sample_app/Allurefile +++ b/sample_app/Allurefile @@ -3,9 +3,6 @@ require 'base64' allure_endpoint = 'https://streamio.testops.cloud' allure_project_id = '135' -# Reassembles the chunked `ALLURE-RESULT::` markers the Dart reporter prints into -# an `allure-results/` dir for allurectl upload. The markers are read from the -# device log: logcat on Android, the simulator log on iOS (device: is a UUID). lane :collect_allure_results do |options| results_dir = options[:output] || "#{root_path}/allure-results" FileUtils.mkdir_p(results_dir) @@ -41,9 +38,6 @@ lane :install_allurectl do path end -# Uploads collected allure-results to Allure TestOps. Requires ALLURE_TOKEN in -# the env. Uses ALLURE_LAUNCH_ID (or launch_id:) to upload into an existing -# launch; otherwise allurectl creates one. lane :allure_upload do |options| results = options[:results] || "#{root_path}/allure-results" unless Dir.exist?(results) && Dir.children(results).any? @@ -60,8 +54,6 @@ lane :allure_upload do |options| UI.success("Uploaded Allure results β†’ #{allure_endpoint} (project #{allure_project_id})") end -# Creates a launch up front and exports its id (ALLURE_LAUNCH_ID) so the nightly -# matrix jobs upload into the same launch. Requires ALLURE_TOKEN. lane :allure_launch do allurectl = install_allurectl branch = ENV['GITHUB_REF_NAME'] || `git rev-parse --abbrev-ref HEAD`.strip @@ -72,7 +64,6 @@ lane :allure_launch do id end -# Deletes the launch (cleanup when a nightly run is cancelled). Requires ALLURE_TOKEN. lane :allure_launch_removal do |options| id = options[:launch_id] || ENV['ALLURE_LAUNCH_ID'] next unless id diff --git a/sample_app/Fastfile b/sample_app/Fastfile index b813ca341b..7591dbfff7 100644 --- a/sample_app/Fastfile +++ b/sample_app/Fastfile @@ -9,28 +9,22 @@ mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4568' github_org = (ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-flutter').split('/').first @force_check = false -# 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 -# iOS simulator ids are UUIDs; Android device ids (emulator-5554, serials) aren't. def ios_device?(device) device.to_s.match?(/\A[0-9A-F]{8}-[0-9A-F]{4}-/i) 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 -# Clones (or reuses via local_server:) the mock-server repo and starts its -# driver in the background. branch: selects the repo branch (default 'main'). lane :start_mock_server do |options| Dir.chdir(root_path) do mock_server_repo = options[:local_server] || mock_server_repo_name @@ -52,14 +46,10 @@ lane :start_mock_server do |options| end end -# Stops the mock-server driver (and any servers it spawned). lane :stop_mock_server do Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil end -# Runs the Patrol e2e suite against a fresh mock server, then tears it down. -# Requires `patrol` on PATH and, on Android, a Gradle-compatible JDK. -# Options: device:, target: (single file), local_server:, mock_server_branch:. lane :run_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) @@ -71,8 +61,6 @@ lane :run_e2e_test do |options| begin Dir.chdir(root_path) do - # Flutter 3.44+ requires assembleDebug + assembleDebugAndroidTest in one - # Gradle invocation; patch patrol_cli before the Android build step. unless ios_device?(options[:device]) sh('bash tool/patch_patrol_cli_android_gradle.sh') end @@ -89,8 +77,6 @@ lane :run_e2e_test do |options| end end -# Builds the e2e test binaries without running them (useful for CI caching). -# `platform` is android or ios. lane :build_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) @@ -105,7 +91,6 @@ lane :build_and_run_e2e_test do |options| run_e2e_test(options) end -# Downloads and installs an iOS simulator runtime (used by cron e2e for iOS 17.x). lane :install_runtime do |options| install_ios_runtime( version: options[:ios], @@ -113,8 +98,6 @@ lane :install_runtime do |options| ) end -# Maps each CI check to the source paths (repo-root-relative) that should -# trigger it; `is_check_required` skips a lane when none of them changed. private_lane :sources_matrix do { e2e: [ diff --git a/sample_app/android/app/build.gradle b/sample_app/android/app/build.gradle index 6e83642df6..8b7d252906 100644 --- a/sample_app/android/app/build.gradle +++ b/sample_app/android/app/build.gradle @@ -34,7 +34,6 @@ android { versionCode flutter.versionCode versionName flutter.versionName - // Patrol e2e testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" } diff --git a/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java b/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java index e4660ed14b..0c2861acf4 100644 --- a/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java +++ b/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java @@ -7,8 +7,6 @@ import pl.leancode.patrol.PatrolJUnitRunner; -// Patrol e2e entry point: enumerates the Dart `patrolTest` cases and runs each -// as a parameterized JUnit test against MainActivity. Do not edit by hand. @RunWith(Parameterized.class) public class MainActivityTest { @Parameterized.Parameters(name = "{0}") diff --git a/sample_app/ios/Podfile b/sample_app/ios/Podfile index 0710354985..07efaa87f8 100644 --- a/sample_app/ios/Podfile +++ b/sample_app/ios/Podfile @@ -31,7 +31,6 @@ target 'Runner' do use_frameworks! use_modular_headers! - # Patrol e2e UI test target. target 'RunnerUITests' do inherit! :complete end diff --git a/sample_app/ios/add_patrol_target.rb b/sample_app/ios/add_patrol_target.rb index a0535b36f5..afc881570f 100644 --- a/sample_app/ios/add_patrol_target.rb +++ b/sample_app/ios/add_patrol_target.rb @@ -18,7 +18,6 @@ :ui_test_bundle, 'RunnerUITests', :ios, deployment_target, nil, :objc ) -# Path is relative to the project dir (ios/). group = project.main_group.find_subpath('RunnerUITests', true) group.set_source_tree('SOURCE_ROOT') file_ref = group.new_reference('RunnerUITests/RunnerUITests.m') @@ -35,18 +34,12 @@ s['CLANG_ENABLE_MODULES'] = 'YES' s['SWIFT_VERSION'] = '5.0' s['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'YES' - # Auto-synthesize Info.plist (UI test bundles need one; without this the - # xcodebuild step fails with code 65). s['GENERATE_INFOPLIST_FILE'] = 'YES' - # Without an explicit product name the .xctest bundle is built nameless, - # which collides as "Multiple commands produce .../PlugIns/.xctest". s['PRODUCT_NAME'] = '$(TARGET_NAME)' end project.save -# Wire the test target into the shared Runner scheme's Test action so -# `patrol`/xcodebuild discovers it. scheme_path = File.join( Xcodeproj::XCScheme.shared_data_dir(project_path), 'Runner.xcscheme' ) diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index 389ac23abf..72ed1cc7ad 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -101,7 +101,6 @@ class _StreamChatSampleAppState extends State void initState() { super.initState(); _notificationService.onNotificationTap = _onNotificationTap; - // Notifications rely on Firebase, which isn't initialized under e2e tests. 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 ea9af72db1..55b6101f35 100644 --- a/sample_app/lib/auth/auth_controller.dart +++ b/sample_app/lib/auth/auth_controller.dart @@ -26,15 +26,11 @@ final _chatPersistenceClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, ); -/// True when running under e2e tests, detected via the presence of a -/// [StreamConnectionOverride]. Used to skip boot-path side effects that assume -/// a full native launch (Firebase, push registration, notifications). bool get isE2eTestRun => authController.debugConnectionOverride != null; Future _sampleAppLogHandler(LogRecord record) async { if (kDebugMode) StreamChatClient.defaultLogHandler(record); - // Crashlytics isn't initialized under e2e tests (the app is pumped in-process). if (isE2eTestRun) return; // report errors to Firebase Crashlytics @@ -47,20 +43,11 @@ Future _sampleAppLogHandler(LogRecord record) async { } } -/// Test-only override that points the [StreamChatClient] at a mock server. -/// -/// `null` in production, so the SDK uses its default base URL. E2E tests set -/// [AuthController.debugConnectionOverride] before the first [AuthController.connect] -/// so the client talks to the local mock server instead of the real backend. @visibleForTesting class StreamConnectionOverride { - /// Creates an override; pass `null` for any field to keep the SDK default. const StreamConnectionOverride({this.baseURL, this.baseWsUrl}); - /// REST base URL, e.g. `http://10.0.2.2:` (Android) / `http://localhost:` (iOS). final String? baseURL; - - /// WebSocket base URL, e.g. `ws://10.0.2.2:` / `ws://localhost:`. final String? baseWsUrl; } @@ -79,7 +66,6 @@ StreamChatClient _buildStreamChatClient( return error is StreamChatNetworkError && error.isRetriable; }, ), - // Null in production β†’ SDK defaults; set by e2e tests to hit the mock server. baseURL: connectionOverride?.baseURL, baseWsUrl: connectionOverride?.baseWsUrl, )..chatPersistenceClient = _chatPersistenceClient; @@ -122,11 +108,6 @@ class AuthController extends ValueNotifier { /// The active client, or `null` before the first [connect]. StreamChatClient? get client => _client; - /// Test-only connection override (see [StreamConnectionOverride]). - /// - /// Set this before the first [connect] in e2e tests to point the client at - /// the local mock server; leave `null` in production. Read once when the - /// client is built, so set it before the app calls [connect]. @visibleForTesting StreamConnectionOverride? debugConnectionOverride; @@ -197,7 +178,6 @@ class AuthController extends ValueNotifier { ]); } - // Push relies on Firebase, which isn't initialized under e2e tests. if (!isE2eTestRun) { _pushTokenManager = PushTokenManager( client: client, @@ -237,12 +217,6 @@ class AuthController extends ValueNotifier { client.disconnectUser(flushChatPersistence: flushPersistence).ignore(); } - /// Resets all session state so the next e2e test starts clean. - /// - /// Patrol runs every test in one app process against this process-wide - /// singleton, so credentials, the client, and the connection override would - /// otherwise leak between tests. Unlike [dispose] this keeps the notifier - /// usable. Call from test teardown. @visibleForTesting Future debugReset() async { _pushTokenManager?.dispose().ignore(); diff --git a/sample_app/pubspec.yaml b/sample_app/pubspec.yaml index b569d17def..941688cf31 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -60,8 +60,6 @@ flutter: - assets/ - pubspec.lock -# Patrol e2e configuration (config lives in pubspec.yaml, not a separate file). -# Tests live in integration_test/ (Patrol defaults to patrol_test/ otherwise). patrol: app_name: Stream Chat test_directory: integration_test diff --git a/sample_app/tool/boot_ios_simulator.sh b/sample_app/tool/boot_ios_simulator.sh index e9697fe5d0..d6b952be6f 100755 --- a/sample_app/tool/boot_ios_simulator.sh +++ b/sample_app/tool/boot_ios_simulator.sh @@ -1,9 +1,4 @@ #!/usr/bin/env bash -# Boots an iOS simulator for Patrol e2e on CI. -# -# Builds use Xcode 26.2 on macos-15; only the simulator runtime varies (see -# stream-chat-swift cron-checks). For downloaded runtimes, pass the simctl name -# only: boot_ios_simulator.sh custom-test-device set -euo pipefail DEVICE_NAME="${1:?usage: boot_ios_simulator.sh [ios-version]}" @@ -65,7 +60,6 @@ echo "device_id=${device_id}" >>"$GITHUB_ENV" xcrun simctl boot "${device_id}" 2>/dev/null || true xcrun simctl bootstatus "${device_id}" -b -# https://github.com/leancodepl/patrol/issues/1282 sleep 10 if [[ -n "$IOS_VERSION" ]]; then diff --git a/sample_app/tool/install_ios_runtime.sh b/sample_app/tool/install_ios_runtime.sh index 3bad4221b6..e3701d3cef 100755 --- a/sample_app/tool/install_ios_runtime.sh +++ b/sample_app/tool/install_ios_runtime.sh @@ -1,6 +1,4 @@ #!/bin/bash -e -# Installs a downloaded iOS simulator runtime image (DMG). -# Ported from stream-chat-swift/Scripts/install_ios_runtime.sh log() { echo "πŸ‘‰ ${1}" >&2; } die() { log "${1}"; exit 1; } diff --git a/sample_app/tool/patch_patrol_cli_android_gradle.sh b/sample_app/tool/patch_patrol_cli_android_gradle.sh index d79d4f3d8f..a6301b462c 100755 --- a/sample_app/tool/patch_patrol_cli_android_gradle.sh +++ b/sample_app/tool/patch_patrol_cli_android_gradle.sh @@ -1,11 +1,4 @@ #!/usr/bin/env bash -# Patches patrol_cli so Android e2e builds run assembleDebug and -# assembleDebugAndroidTest in a single Gradle invocation. -# -# Flutter 3.44's shouldConfigureFlutterTask skips registering -# compileFlutterBuildDebug when only assembleDebugAndroidTest is requested, -# which breaks Patrol's two-step Gradle build on CI. -# See: https://github.com/flutter/flutter/issues/109560 set -euo pipefail PATROL_VERSION="${PATROL_CLI_VERSION:-$(dart pub global list 2>/dev/null | awk '/^patrol_cli / {print $2; exit}')}" @@ -88,9 +81,7 @@ old = """ // :app:assembleDebug throw Exception(cause); }""" -new = """ // :app:assembleDebug + :app:assembleDebugAndroidTest in one invocation - // so Flutter registers compileFlutterBuildDebug (Flutter 3.44+). - // Patched by sample_app/tool/patch_patrol_cli_android_gradle.sh +new = """ // Patched by sample_app/tool/patch_patrol_cli_android_gradle.sh final assembleInvocation = options.toGradleAssembleInvocation( isWindows: _platform.isWindows, @@ -141,5 +132,4 @@ print(f"Patched {path}") PY fi -# Drop the precompiled snapshot so `patrol` runs from the patched sources. rm -f "${PUB_CACHE}/global_packages/patrol_cli/bin/"*.snapshot 2>/dev/null || true From a5e634266546d776604f8b95627ea475a75e943d Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 10:52:42 +0100 Subject: [PATCH 03/33] Move things around a little bit --- .github/actions/setup-ios-runtime/action.yml | 5 +- .github/workflows/distribute_external.yml | 4 +- .github/workflows/distribute_internal.yml | 6 +- .github/workflows/e2e_test.yml | 4 +- .github/workflows/e2e_test_cron.yml | 8 +- .github/workflows/stream_flutter_workflow.yml | 2 +- sample_app/{android => }/.ruby-version | 0 sample_app/Gemfile | 15 + sample_app/Gemfile.lock | 555 ++++++++++++++++++ sample_app/android/Gemfile | 7 - sample_app/android/Gemfile.lock | 266 --------- sample_app/android/fastlane/Appfile | 2 +- sample_app/android/fastlane/Fastfile | 2 +- sample_app/{ => fastlane}/Allurefile | 0 sample_app/{ => fastlane}/Fastfile | 12 +- sample_app/{android => }/fastlane/Pluginfile | 0 sample_app/ios/.ruby-version | 1 - sample_app/ios/Gemfile | 8 - sample_app/ios/Gemfile.lock | 329 ----------- sample_app/ios/fastlane/Fastfile | 2 +- sample_app/ios/fastlane/Pluginfile | 6 - 21 files changed, 592 insertions(+), 642 deletions(-) rename sample_app/{android => }/.ruby-version (100%) create mode 100644 sample_app/Gemfile create mode 100644 sample_app/Gemfile.lock delete mode 100644 sample_app/android/Gemfile delete mode 100644 sample_app/android/Gemfile.lock rename sample_app/{ => fastlane}/Allurefile (100%) rename sample_app/{ => fastlane}/Fastfile (89%) rename sample_app/{android => }/fastlane/Pluginfile (100%) delete mode 100644 sample_app/ios/.ruby-version delete mode 100644 sample_app/ios/Gemfile delete mode 100644 sample_app/ios/Gemfile.lock delete mode 100644 sample_app/ios/fastlane/Pluginfile diff --git a/.github/actions/setup-ios-runtime/action.yml b/.github/actions/setup-ios-runtime/action.yml index 9493dcb741..9c479ea80d 100644 --- a/.github/actions/setup-ios-runtime/action.yml +++ b/.github/actions/setup-ios-runtime/action.yml @@ -25,11 +25,10 @@ runs: - name: Setup iOS simulator runtime shell: bash - working-directory: sample_app/ios run: | sudo rm -rfv ~/Library/Developer/CoreSimulator/* || true - bundle exec fastlane install_runtime ios:${{ inputs.version }} - sudo rm -rfv *.dmg || true + cd sample_app/ios && bundle exec fastlane install_runtime ios:${{ inputs.version }} + sudo rm -rfv sample_app/*.dmg || true xcrun simctl list runtimes - name: Create custom iOS simulator diff --git a/.github/workflows/distribute_external.yml b/.github/workflows/distribute_external.yml index 3e969e84db..429a15ccc6 100644 --- a/.github/workflows/distribute_external.yml +++ b/.github/workflows/distribute_external.yml @@ -80,7 +80,7 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - name: Distribute to S3 working-directory: sample_app/android @@ -124,7 +124,7 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - working-directory: sample_app/ios + working-directory: sample_app - 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..7adbcea4d4 100644 --- a/.github/workflows/distribute_internal.yml +++ b/.github/workflows/distribute_internal.yml @@ -90,7 +90,7 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - name: Setup Firebase Service Account' working-directory: sample_app/android @@ -140,7 +140,7 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - working-directory: sample_app/ios + working-directory: sample_app - name: Setup Firebase Service Account working-directory: sample_app/ios @@ -193,7 +193,7 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - working-directory: sample_app/ios + working-directory: sample_app - name: Distribute to TestFlight Internal working-directory: sample_app/ios diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 80880ee03f..43a3c13361 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -40,7 +40,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - name: Bootstrap run: | @@ -95,7 +95,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/ios + working-directory: sample_app - name: Bootstrap run: | diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 908637d302..e9616ecda4 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -34,7 +34,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - id: launch env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} @@ -67,7 +67,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - name: Bootstrap run: | @@ -138,7 +138,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/ios + working-directory: sample_app - name: Bootstrap run: | @@ -198,7 +198,7 @@ jobs: with: ruby-version: "3.3" bundler-cache: true - working-directory: sample_app/android + working-directory: sample_app - name: Remove Allure launch env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 5877a89f16..f1f272bf4c 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -209,7 +209,7 @@ jobs: - name: Setup Ruby and Gems uses: ruby/setup-ruby@v1 with: - working-directory: ${{ matrix.working-directory }} + working-directory: sample_app bundler-cache: true - name: "Build ${{ matrix.platform }} App" working-directory: ${{ matrix.working-directory }} 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/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..ccc26ac958 --- /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.5) + 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 + 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.5) sha256=6b69093c2e1156b39414d1625ccabf8555a84005633f225708f591a3bcc38aa8 + 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/Gemfile.lock b/sample_app/android/Gemfile.lock deleted file mode 100644 index 1505cbe655..0000000000 --- a/sample_app/android/Gemfile.lock +++ /dev/null @@ -1,266 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.8) - abbrev (0.1.2) - addressable (2.9.0) - public_suffix (>= 2.0.2, < 8.0) - apktools (0.7.5) - rubyzip (~> 2.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1253.0) - aws-sdk-core (3.249.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.128.0) - aws-sdk-core (~> 3, >= 3.248.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.224.0) - aws-sdk-core (~> 3, >= 3.248.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) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - 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) - emoji_regex (3.2.3) - excon (0.112.0) - faraday (1.10.5) - 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.235.0) - CFPropertyList (>= 2.3, < 5.0.0) - abbrev (~> 0.1) - addressable (>= 2.8, < 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, < 1.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.1.0, < 4) - logger (>= 1.6, < 2.0) - mini_magick (>= 4.9.4, < 5.0.0) - 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.3) - xctest_list (= 1.2.1) - fastlane-sirp (1.1.0) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.101.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.19.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-firebaseappdistribution_v1alpha (0.28.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-iamcredentials_v1 (0.27.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.62.0) - google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.8.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.6.0) - google-cloud-storage (1.60.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.16.2) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.2) - google-logging-utils (~> 0.1) - jwt (>= 1.4, < 4.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.8) - domain_name (~> 0.5) - httpclient (2.9.0) - mutex_m - jmespath (1.6.2) - json (2.19.5) - 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.0414) - mini_magick (4.13.2) - mini_mime (1.1.5) - multi_json (1.21.1) - multipart-post (2.4.1) - mutex_m (0.3.0) - nanaimo (0.4.0) - naturally (2.3.0) - nkf (0.2.0) - optparse (0.8.1) - os (1.1.4) - ostruct (0.6.3) - plist (3.7.2) - public_suffix (4.0.7) - 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) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - security (0.1.5) - signet (0.21.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - 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 - arm64-darwin-25 - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-aws_s3 - fastlane-plugin-firebase_app_distribution - fastlane-plugin-stream_actions - multi_json - -BUNDLED WITH - 2.6.8 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..ae9d518861 100644 --- a/sample_app/android/fastlane/Fastfile +++ b/sample_app/android/fastlane/Fastfile @@ -1,5 +1,5 @@ skip_docs -import '../../Fastfile' +import '../../fastlane/Fastfile' default_platform(:android) before_all do diff --git a/sample_app/Allurefile b/sample_app/fastlane/Allurefile similarity index 100% rename from sample_app/Allurefile rename to sample_app/fastlane/Allurefile diff --git a/sample_app/Fastfile b/sample_app/fastlane/Fastfile similarity index 89% rename from sample_app/Fastfile rename to sample_app/fastlane/Fastfile index 7591dbfff7..bd70ea8b30 100644 --- a/sample_app/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -2,7 +2,7 @@ opt_out_usage require 'net/http' -import '../../Allurefile' +import File.expand_path('Allurefile', __dir__) mock_server_repo_name = 'stream-chat-test-mock-server' mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4568' @@ -38,10 +38,7 @@ lane :start_mock_server do |options| Dir.chdir(mock_server_repo) do FileUtils.mkdir_p('logs') - Bundler.with_unbundled_env do - sh('bundle install') - sh("bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 &") - end + sh("bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 &") end end end @@ -111,7 +108,8 @@ private_lane :sources_matrix do 'sample_app/tool/install_ios_runtime.sh', '.github/actions/setup-ios-runtime', ], - ruby: ['sample_app/Fastfile', 'sample_app/Allurefile', 'sample_app/android/fastlane', - 'sample_app/ios/fastlane', 'sample_app/android/Gemfile', 'sample_app/ios/Gemfile'], + ruby: ['sample_app/fastlane/Fastfile', 'sample_app/fastlane/Allurefile', 'sample_app/Gemfile', + 'sample_app/.ruby-version', 'sample_app/fastlane/Pluginfile', 'sample_app/android/fastlane', + 'sample_app/ios/fastlane'], } end diff --git a/sample_app/android/fastlane/Pluginfile b/sample_app/fastlane/Pluginfile similarity index 100% rename from sample_app/android/fastlane/Pluginfile rename to sample_app/fastlane/Pluginfile 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/Gemfile.lock b/sample_app/ios/Gemfile.lock deleted file mode 100644 index ad8148083b..0000000000 --- a/sample_app/ios/Gemfile.lock +++ /dev/null @@ -1,329 +0,0 @@ -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) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1253.0) - aws-sdk-core (3.249.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.128.0) - aws-sdk-core (~> 3, >= 3.248.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.224.0) - aws-sdk-core (~> 3, >= 3.248.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.16.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.16.2) - 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) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.27.0, < 2.0) - cocoapods-core (1.16.2) - 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.6) - 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) - escape (0.0.4) - ethon (0.18.0) - ffi (>= 1.15.0) - logger - excon (0.112.0) - faraday (1.10.5) - 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.235.0) - CFPropertyList (>= 2.3, < 5.0.0) - abbrev (~> 0.1) - addressable (>= 2.8, < 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, < 1.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.1.0, < 4) - logger (>= 1.6, < 2.0) - mini_magick (>= 4.9.4, < 5.0.0) - 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-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.3) - xctest_list (= 1.2.1) - fastlane-sirp (1.1.0) - ffi (1.17.4-arm64-darwin) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.101.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.19.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-firebaseappdistribution_v1alpha (0.28.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-iamcredentials_v1 (0.27.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) - google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.62.0) - google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.8.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.6.0) - google-cloud-storage (1.60.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.16.2) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.2) - google-logging-utils (~> 0.1) - jwt (>= 1.4, < 4.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - 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.14.8) - concurrent-ruby (~> 1.0) - jmespath (1.6.2) - json (2.19.5) - jwt (3.2.0) - base64 - logger (1.7.0) - 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) - mutex_m (0.3.0) - nanaimo (0.4.0) - nap (1.1.0) - naturally (2.3.0) - netrc (0.11.0) - nkf (0.2.0) - optparse (0.8.1) - os (1.1.4) - ostruct (0.6.3) - plist (3.7.2) - public_suffix (4.0.7) - 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 (2.5.1) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - securerandom (0.4.1) - security (0.1.5) - signet (0.21.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - 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) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - 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 - arm64-darwin - -DEPENDENCIES - cocoapods - fastlane - fastlane-plugin-firebase_app_distribution - fastlane-plugin-stream_actions - multi_json - -BUNDLED WITH - 2.6.8 diff --git a/sample_app/ios/fastlane/Fastfile b/sample_app/ios/fastlane/Fastfile index 88687c7aa8..d4245d6b12 100644 --- a/sample_app/ios/fastlane/Fastfile +++ b/sample_app/ios/fastlane/Fastfile @@ -1,5 +1,5 @@ skip_docs -import '../../Fastfile' +import '../../fastlane/Fastfile' default_platform(:ios) before_all do 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' From f108d0f80873de3c6d7fe9ba5171d889f00c2ebf Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 11:20:02 +0100 Subject: [PATCH 04/33] Bump actions versions --- .github/workflows/e2e_test.yml | 6 +++--- .github/workflows/e2e_test_cron.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 43a3c13361..65ffcbd703 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" @@ -68,7 +68,7 @@ jobs: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} run: cd sample_app/android && bundle exec fastlane allure_upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: e2e-android-logs @@ -128,7 +128,7 @@ jobs: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} run: cd sample_app/ios && bundle exec fastlane allure_upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: always() with: name: e2e-ios-logs diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index e9616ecda4..32999f94ef 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -52,7 +52,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" @@ -97,7 +97,7 @@ jobs: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} run: cd sample_app/android && bundle exec fastlane allure_upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: failure() with: name: e2e-nightly-android-${{ matrix.api-level }} @@ -182,7 +182,7 @@ jobs: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} run: cd sample_app/ios && bundle exec fastlane allure_upload - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: failure() with: name: e2e-nightly-ios-${{ matrix.ios }} From 3783d7cf9b5640a73393658c8968f7478384c37a Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 11:35:29 +0100 Subject: [PATCH 05/33] Fix iOS --- .../integration_test/mock_server/mock_server.dart | 11 ++++++----- sample_app/ios/Runner/Info.plist | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index bfd360c056..6f9565f4fa 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -10,12 +10,13 @@ class MockServer { final String url; final String wsUrl; - static String get _host => Platform.isAndroid ? '10.0.2.2' : 'localhost'; + 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); static Future start({String? testName}) async { final name = testName ?? _currentTestName(); @@ -51,11 +52,11 @@ class MockServer { Future get(String endpoint) => _get('$url/$endpoint'); Future waitUntilReady({ - Duration timeout = const Duration(seconds: 15), + Duration timeout = const Duration(seconds: 45), }) async { final deadline = DateTime.now().add(timeout); while (DateTime.now().isBefore(deadline)) { - final ready = await _statusCode('$url/ping') + final ready = await _statusCode('$url/ping', timeout: _pingTimeout) .then((code) => code == 200) .catchError((Object _) => false); if (ready) return; @@ -75,8 +76,8 @@ class MockServer { } } - static Future _statusCode(String url) async { - final client = HttpClient()..connectionTimeout = _httpTimeout; + 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); 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 From 994903399364c358f962675ed6b6c3842eb1ff54 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 12:01:55 +0100 Subject: [PATCH 06/33] Bump cache version --- .github/workflows/e2e_test.yml | 2 +- .github/workflows/e2e_test_cron.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 65ffcbd703..bd3ff01310 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -105,7 +105,7 @@ jobs: dart pub global activate patrol_cli - name: Cache CocoaPods - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | sample_app/ios/Pods diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 32999f94ef..e5c8f1a1fd 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -155,7 +155,7 @@ jobs: device: ${{ matrix.device }} - name: Cache CocoaPods - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | sample_app/ios/Pods From ef1e0ca33a43b4487894b975b47f4f2361f91242 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 12:02:10 +0100 Subject: [PATCH 07/33] Trying to speed up ios build --- .github/workflows/e2e_test.yml | 19 +++++++++++++++++-- .github/workflows/e2e_test_cron.yml | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index bd3ff01310..57f753ffd2 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -78,6 +78,8 @@ jobs: ios: runs-on: macos-26 + env: + COMPILER_INDEX_STORE_ENABLE: NO steps: - uses: actions/checkout@v6 @@ -101,7 +103,7 @@ jobs: run: | flutter precache --ios flutter pub global activate melos - melos bootstrap + melos bootstrap --category=packages --category=sample_app dart pub global activate patrol_cli - name: Cache CocoaPods @@ -109,10 +111,23 @@ jobs: with: path: | sample_app/ios/Pods - ~/Library/Caches/CocoaPods + ${{ env.HOME }}/Library/Caches/CocoaPods key: pods-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ runner.os }}- + - name: Cache Patrol iOS build + uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}- + patrol-ios-integ-${{ runner.os }}- + + - name: Install CocoaPods + working-directory: sample_app/ios + run: pod install + - name: Boot simulator run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index e5c8f1a1fd..4107d7b0a6 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -121,6 +121,7 @@ jobs: setup_runtime: true env: ALLURE_LAUNCH_ID: ${{ needs.setup.outputs.launch_id }} + COMPILER_INDEX_STORE_ENABLE: NO steps: - uses: actions/checkout@v6 @@ -144,7 +145,7 @@ jobs: run: | flutter precache --ios flutter pub global activate melos - melos bootstrap + melos bootstrap --category=packages --category=sample_app dart pub global activate patrol_cli - uses: ./.github/actions/setup-ios-runtime @@ -159,10 +160,23 @@ jobs: with: path: | sample_app/ios/Pods - ~/Library/Caches/CocoaPods + ${{ env.HOME }}/Library/Caches/CocoaPods key: pods-${{ matrix.ios }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ matrix.ios }}- + - name: Cache Patrol iOS build + uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-26.2-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-26.2- + patrol-ios-integ-${{ runner.os }}- + + - name: Install CocoaPods + working-directory: sample_app/ios + run: pod install + - name: Boot simulator run: | if [ "${{ matrix.setup_runtime }}" = "true" ]; then From ab3f3ffb3344bae91683d009eb3f438d6f3ec57c Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 13:11:06 +0100 Subject: [PATCH 08/33] Fix nohup --- sample_app/fastlane/Fastfile | 69 ++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index bd70ea8b30..f0bb60cab4 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -25,6 +25,19 @@ lane :fetch_dependencies do sh_on_root(command: "flutter pub get --suppress-analytics") end +def wait_for_mock_driver(port: mock_server_driver_port, timeout: 30) + deadline = Time.now + timeout + uri = URI("http://127.0.0.1:#{port}/") + loop do + Net::HTTP.get_response(uri) + return + rescue StandardError + raise "Mock driver did not start within #{timeout}s" if Time.now >= deadline + + sleep 0.5 + end +end + lane :start_mock_server do |options| Dir.chdir(root_path) do mock_server_repo = options[:local_server] || mock_server_repo_name @@ -38,7 +51,7 @@ lane :start_mock_server do |options| 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 &") + sh("nohup bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 '8081', + 'TEST_RUNNER_PATROL_APP_PORT' => '8082', + }, + 'xcodebuild', 'test-without-building', + '-xctestrun', xctestrun, + '-only-testing', 'RunnerUITests/RunnerUITests', + '-destination', "platform=iOS Simulator,id=#{options[:device]}", + '-destination-timeout', '1', + '-resultBundlePath', result_bundle, + ) + end +end + lane :build_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) From fd68c101e229d3ea621f3c0a1273e3c5b7cc5719 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 13:31:28 +0100 Subject: [PATCH 09/33] Fix patrol --- sample_app/fastlane/Fastfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index f0bb60cab4..98bee78437 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -66,11 +66,10 @@ lane :run_e2e_test do |options| begin Dir.chdir(root_path) do if ios_device?(options[:device]) - ios_flags = [] - ios_flags << "--target #{options[:target]}" if options[:target] - ios_flags << "--device #{options[:device]}" - ios_flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" - sh("patrol build ios --simulator #{ios_flags.join(' ')}".strip) + ios_build_flags = [] + ios_build_flags << "--target #{options[:target]}" if options[:target] + ios_build_flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" + sh("patrol build ios --simulator #{ios_build_flags.join(' ')}".strip) start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) wait_for_mock_driver From 66f55737d3d43fa3abe4c7d9ec04c6db03f16e5a Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 15:33:24 +0100 Subject: [PATCH 10/33] FIx fastlane --- sample_app/fastlane/Fastfile | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 98bee78437..2831fa3774 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -25,19 +25,6 @@ lane :fetch_dependencies do sh_on_root(command: "flutter pub get --suppress-analytics") end -def wait_for_mock_driver(port: mock_server_driver_port, timeout: 30) - deadline = Time.now + timeout - uri = URI("http://127.0.0.1:#{port}/") - loop do - Net::HTTP.get_response(uri) - return - rescue StandardError - raise "Mock driver did not start within #{timeout}s" if Time.now >= deadline - - sleep 0.5 - end -end - lane :start_mock_server do |options| Dir.chdir(root_path) do mock_server_repo = options[:local_server] || mock_server_repo_name @@ -92,6 +79,21 @@ lane :run_e2e_test do |options| end end +private_lane :wait_for_mock_driver do |options| + port = (options[:port] || ENV['MOCK_DRIVER_PORT'] || '4568').to_s + timeout = (options[:timeout] || 30).to_i + deadline = Time.now + timeout + uri = URI("http://127.0.0.1:#{port}/") + loop do + Net::HTTP.get_response(uri) + break + rescue StandardError + UI.user_error!("Mock driver did not start within #{timeout}s") if Time.now >= deadline + + sleep 0.5 + end +end + private_lane :run_ios_patrol_tests do |options| products = File.join(root_path, 'build/ios_integ/Build/Products') xctestrun = Dir[File.join(products, 'Runner_*iphonesimulator*.xctestrun')].max_by { |f| File.mtime(f) } From f98950bba7d44382810b9e3d20f902c4fe299431 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 16:11:01 +0100 Subject: [PATCH 11/33] Revert "FIx fastlane" This reverts commit 66f55737d3d43fa3abe4c7d9ec04c6db03f16e5a. --- sample_app/fastlane/Fastfile | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 2831fa3774..98bee78437 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -25,6 +25,19 @@ lane :fetch_dependencies do sh_on_root(command: "flutter pub get --suppress-analytics") end +def wait_for_mock_driver(port: mock_server_driver_port, timeout: 30) + deadline = Time.now + timeout + uri = URI("http://127.0.0.1:#{port}/") + loop do + Net::HTTP.get_response(uri) + return + rescue StandardError + raise "Mock driver did not start within #{timeout}s" if Time.now >= deadline + + sleep 0.5 + end +end + lane :start_mock_server do |options| Dir.chdir(root_path) do mock_server_repo = options[:local_server] || mock_server_repo_name @@ -79,21 +92,6 @@ lane :run_e2e_test do |options| end end -private_lane :wait_for_mock_driver do |options| - port = (options[:port] || ENV['MOCK_DRIVER_PORT'] || '4568').to_s - timeout = (options[:timeout] || 30).to_i - deadline = Time.now + timeout - uri = URI("http://127.0.0.1:#{port}/") - loop do - Net::HTTP.get_response(uri) - break - rescue StandardError - UI.user_error!("Mock driver did not start within #{timeout}s") if Time.now >= deadline - - sleep 0.5 - end -end - private_lane :run_ios_patrol_tests do |options| products = File.join(root_path, 'build/ios_integ/Build/Products') xctestrun = Dir[File.join(products, 'Runner_*iphonesimulator*.xctestrun')].max_by { |f| File.mtime(f) } From 1f3f787ec14becf1acec6ae6e5e566e001bd46aa Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 16:11:06 +0100 Subject: [PATCH 12/33] Revert "Fix patrol" This reverts commit fd68c101e229d3ea621f3c0a1273e3c5b7cc5719. --- sample_app/fastlane/Fastfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 98bee78437..f0bb60cab4 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -66,10 +66,11 @@ lane :run_e2e_test do |options| begin Dir.chdir(root_path) do if ios_device?(options[:device]) - ios_build_flags = [] - ios_build_flags << "--target #{options[:target]}" if options[:target] - ios_build_flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" - sh("patrol build ios --simulator #{ios_build_flags.join(' ')}".strip) + ios_flags = [] + ios_flags << "--target #{options[:target]}" if options[:target] + ios_flags << "--device #{options[:device]}" + ios_flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" + sh("patrol build ios --simulator #{ios_flags.join(' ')}".strip) start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) wait_for_mock_driver From 3185515f4d17ac66ba9bd7733c0d350194bef846 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 16:11:12 +0100 Subject: [PATCH 13/33] Revert "Fix nohup" This reverts commit ab3f3ffb3344bae91683d009eb3f438d6f3ec57c. --- sample_app/fastlane/Fastfile | 69 ++++++++---------------------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index f0bb60cab4..bd70ea8b30 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -25,19 +25,6 @@ lane :fetch_dependencies do sh_on_root(command: "flutter pub get --suppress-analytics") end -def wait_for_mock_driver(port: mock_server_driver_port, timeout: 30) - deadline = Time.now + timeout - uri = URI("http://127.0.0.1:#{port}/") - loop do - Net::HTTP.get_response(uri) - return - rescue StandardError - raise "Mock driver did not start within #{timeout}s" if Time.now >= deadline - - sleep 0.5 - end -end - lane :start_mock_server do |options| Dir.chdir(root_path) do mock_server_repo = options[:local_server] || mock_server_repo_name @@ -51,7 +38,7 @@ lane :start_mock_server do |options| Dir.chdir(mock_server_repo) do FileUtils.mkdir_p('logs') - sh("nohup bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 logs/driver.log 2>&1 &") end end end @@ -63,29 +50,23 @@ end lane :run_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) + start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) + unless ios_device?(options[:device]) + serial = options[:device] ? "-s #{options[:device]} " : '' + sh("adb #{serial}logcat -c") + end + begin Dir.chdir(root_path) do - if ios_device?(options[:device]) - ios_flags = [] - ios_flags << "--target #{options[:target]}" if options[:target] - ios_flags << "--device #{options[:device]}" - ios_flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" - sh("patrol build ios --simulator #{ios_flags.join(' ')}".strip) - - start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) - wait_for_mock_driver - run_ios_patrol_tests(device: options[:device]) - else - start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) - serial = options[:device] ? "-s #{options[:device]} " : '' - sh("adb #{serial}logcat -c") + unless ios_device?(options[:device]) sh('bash tool/patch_patrol_cli_android_gradle.sh') - - flags = [] - flags << "--target #{options[:target]}" if options[:target] - flags << "--device #{options[:device]}" if options[:device] - sh("patrol test #{flags.join(' ')}".strip) end + + flags = [] + flags << "--target #{options[:target]}" if options[:target] + flags << "--device #{options[:device]}" if options[:device] + flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" if ios_device?(options[:device]) + sh("patrol test #{flags.join(' ')}".strip) end ensure collect_allure_results(device: options[:device]) rescue nil @@ -93,28 +74,6 @@ lane :run_e2e_test do |options| end end -private_lane :run_ios_patrol_tests do |options| - products = File.join(root_path, 'build/ios_integ/Build/Products') - xctestrun = Dir[File.join(products, 'Runner_*iphonesimulator*.xctestrun')].max_by { |f| File.mtime(f) } - UI.user_error!("No .xctestrun in #{products}; run patrol build ios first") if xctestrun.nil? - - result_bundle = File.join(root_path, "build/ios_results_#{Time.now.to_i}.xcresult") - Dir.chdir(File.join(root_path, 'ios')) do - sh( - { - 'TEST_RUNNER_PATROL_TEST_PORT' => '8081', - 'TEST_RUNNER_PATROL_APP_PORT' => '8082', - }, - 'xcodebuild', 'test-without-building', - '-xctestrun', xctestrun, - '-only-testing', 'RunnerUITests/RunnerUITests', - '-destination', "platform=iOS Simulator,id=#{options[:device]}", - '-destination-timeout', '1', - '-resultBundlePath', result_bundle, - ) - end -end - lane :build_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) From 164faf0bf3597cb58051670354a3c7bc8034d091 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 17:43:48 +0100 Subject: [PATCH 14/33] Logs --- .github/workflows/e2e_test.yml | 4 ++-- sample_app/fastlane/Fastfile | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 57f753ffd2..113736f2fd 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -60,7 +60,7 @@ jobs: api-level: 34 arch: x86_64 profile: pixel_6 - script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 mock_server_branch:main + script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 - name: Upload Allure results if: always() @@ -135,7 +135,7 @@ jobs: env: IOS_SIMULATOR_VERSION: ${{ env.ios_simulator_version }} run: | - cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} mock_server_branch:main + cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} - name: Upload Allure results if: always() diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index bd70ea8b30..8450550103 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -65,6 +65,7 @@ lane :run_e2e_test do |options| flags = [] flags << "--target #{options[:target]}" if options[:target] flags << "--device #{options[:device]}" if options[:device] + flags << "--verbose" if options[:verbose] flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" if ios_device?(options[:device]) sh("patrol test #{flags.join(' ')}".strip) end From 76ecd110f246a9f75dec0c40efa1437a094b4e49 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 17:59:23 +0100 Subject: [PATCH 15/33] Some iOS updates --- .github/workflows/e2e_test.yml | 4 ---- .github/workflows/e2e_test_cron.yml | 5 +---- sample_app/android/fastlane/Fastfile | 3 ++- sample_app/fastlane/Fastfile | 19 +++++++++++++++++-- sample_app/ios/fastlane/Fastfile | 4 ++++ 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 113736f2fd..8836eac5e2 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -83,10 +83,6 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ env.ios_simulator_version }} - - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.flutter_version }} diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 4107d7b0a6..e200185a38 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -120,15 +120,12 @@ jobs: device: "iPhone 15 Pro" setup_runtime: true env: + XCODE_VERSION: "26.2" ALLURE_LAUNCH_ID: ${{ needs.setup.outputs.launch_id }} COMPILER_INDEX_STORE_ENABLE: NO steps: - uses: actions/checkout@v6 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "26.2" - - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.flutter_version }} diff --git a/sample_app/android/fastlane/Fastfile b/sample_app/android/fastlane/Fastfile index ae9d518861..e7e1aaaa5d 100644 --- a/sample_app/android/fastlane/Fastfile +++ b/sample_app/android/fastlane/Fastfile @@ -4,6 +4,7 @@ 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/Fastfile b/sample_app/fastlane/Fastfile index 8450550103..bc6ac47167 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -1,6 +1,7 @@ opt_out_usage require 'net/http' +require 'json' import File.expand_path('Allurefile', __dir__) @@ -17,6 +18,16 @@ def ios_device?(device) device.to_s.match?(/\A[0-9A-F]{8}-[0-9A-F]{4}-/i) end +def ios_simulator_runtime_version(udid) + data = JSON.parse(`xcrun simctl list devices available --json 2>/dev/null`) + data['devices'].each do |runtime, devices| + next unless devices.any? { |d| d['udid'] == udid } + + return runtime.split('iOS-').last.tr('-', '.') + end + nil +end + lane :sh_on_root do |options| Dir.chdir(root_path) { sh(options[:command]) } end @@ -65,8 +76,12 @@ lane :run_e2e_test do |options| flags = [] flags << "--target #{options[:target]}" if options[:target] flags << "--device #{options[:device]}" if options[:device] - flags << "--verbose" if options[:verbose] - flags << "--ios #{ENV.fetch('IOS_SIMULATOR_VERSION', '26.2')}" if ios_device?(options[:device]) + flags << '--verbose' if options[:verbose] + if ios_device?(options[:device]) + ios_version = options[:ios] || ENV['IOS_SIMULATOR_VERSION'] || + ios_simulator_runtime_version(options[:device]) + flags << "--ios #{ios_version}" if ios_version && !ios_version.empty? + end sh("patrol test #{flags.join(' ')}".strip) end ensure diff --git a/sample_app/ios/fastlane/Fastfile b/sample_app/ios/fastlane/Fastfile index d4245d6b12..a10c69de2f 100644 --- a/sample_app/ios/fastlane/Fastfile +++ b/sample_app/ios/fastlane/Fastfile @@ -2,9 +2,13 @@ skip_docs import '../../fastlane/Fastfile' default_platform(:ios) +xcode_version = ENV['XCODE_VERSION'] || '26.2' + before_all do if is_ci setup_ci + setup_git_config + select_xcode(version: xcode_version) end end From 5f0ad9ab7e95e369cc55a4ccfadb8111ed0bc90b Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 17:59:39 +0100 Subject: [PATCH 16/33] Comment android --- .github/workflows/e2e_test.yml | 110 ++++++++++++++++----------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 8836eac5e2..767e1b20a2 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -20,61 +20,61 @@ env: ios_simulator_device: "iPhone 17" jobs: - android: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: "17" - - - 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: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - bundler-cache: true - working-directory: sample_app - - - name: Bootstrap - run: | - flutter pub global activate melos - melos bootstrap - dart pub global activate patrol_cli - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Run e2e (Android emulator) - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 34 - 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: always() - env: - ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} - run: cd sample_app/android && bundle exec fastlane allure_upload - - - uses: actions/upload-artifact@v7 - if: always() - with: - name: e2e-android-logs - path: | - sample_app/stream-chat-test-mock-server/logs - sample_app/build/app/reports + # android: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + + # - uses: actions/setup-java@v5 + # with: + # distribution: temurin + # java-version: "17" + + # - 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: ruby/setup-ruby@v1 + # with: + # ruby-version: "3.3" + # bundler-cache: true + # working-directory: sample_app + + # - name: Bootstrap + # run: | + # flutter pub global activate melos + # melos bootstrap + # dart pub global activate patrol_cli + + # - name: Enable KVM + # run: | + # echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + # sudo udevadm control --reload-rules + # sudo udevadm trigger --name-match=kvm + + # - name: Run e2e (Android emulator) + # uses: reactivecircus/android-emulator-runner@v2 + # with: + # api-level: 34 + # 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: always() + # env: + # ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + # run: cd sample_app/android && bundle exec fastlane allure_upload + + # - uses: actions/upload-artifact@v7 + # if: always() + # with: + # name: e2e-android-logs + # path: | + # sample_app/stream-chat-test-mock-server/logs + # sample_app/build/app/reports ios: runs-on: macos-26 From be0ffc5670877d9ecff4cb45bfb2ecb1c9c3fc11 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 18:31:53 +0100 Subject: [PATCH 17/33] Add allure id --- .../integration_test/message_list_test.dart | 36 ++++++++++--------- .../support/stream_test_case.dart | 8 ++--- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/sample_app/integration_test/message_list_test.dart b/sample_app/integration_test/message_list_test.dart index deb8261d77..bc9a984dde 100644 --- a/sample_app/integration_test/message_list_test.dart +++ b/sample_app/integration_test/message_list_test.dart @@ -8,23 +8,27 @@ import 'support/stream_test_env.dart'; void main() { const sampleText = 'Test'; - streamTest('message list updates when the user sends a message', ($) async { - final env = StreamTestEnv(); - await env.setUp($); - addTearDown(env.tearDown); + streamTest( + allureId: '11188', + description: 'message list updates when the user sends a message', + body: ($) async { + final env = StreamTestEnv(); + await env.setUp($); + addTearDown(env.tearDown); - await step('GIVEN the user opens a channel', () async { - await env.backendRobot.generateChannels(channelsCount: 1); - await env.userRobot.login(); - await env.userRobot.openChannel(); - }); + await step('GIVEN the user opens a channel', () async { + await env.backendRobot.generateChannels(channelsCount: 1); + await env.userRobot.login(); + await env.userRobot.openChannel(); + }); - await step('WHEN the user sends a message', () async { - await env.userRobot.sendMessage(sampleText); - }); + await step('WHEN the user sends a message', () async { + await env.userRobot.sendMessage(sampleText); + }); - await step('THEN the message is displayed', () async { - await env.userRobot.assertMessage(sampleText); - }); - }); + await step('THEN the message is displayed', () async { + await env.userRobot.assertMessage(sampleText); + }); + }, + ); } diff --git a/sample_app/integration_test/support/stream_test_case.dart b/sample_app/integration_test/support/stream_test_case.dart index 99c4c3cad0..80227ed3a3 100644 --- a/sample_app/integration_test/support/stream_test_case.dart +++ b/sample_app/integration_test/support/stream_test_case.dart @@ -6,10 +6,10 @@ import 'package:test_api/src/backend/invoker.dart' show Invoker; import '../allure/allure.dart'; -void streamTest( - String description, - Future Function(PatrolIntegrationTester $) callback, { +void streamTest({ String? allureId, + required String description, + required Future Function(PatrolIntegrationTester $) body, }) { patrolTest(description, ($) async { Allure.instance.startTest( @@ -18,7 +18,7 @@ void streamTest( labels: {if (allureId != null) 'AS_ID': allureId}, ); try { - await callback($); + await body($); Allure.instance.stopTest(status: AllureStatus.passed); } on TestFailure catch (e, st) { Allure.instance.stopTest(status: AllureStatus.failed, message: e, trace: st); From b28272bff4409ced2c16be789feb30202bed982e Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 18:56:22 +0100 Subject: [PATCH 18/33] fix(e2e): await mock server response body before closing client --- .../mock_server/mock_server.dart | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index 6f9565f4fa..fc2c35a692 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -17,6 +17,8 @@ class MockServer { static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); + static const _retries = 3; + static const _retryBackoff = Duration(milliseconds: 250); static Future start({String? testName}) async { final name = testName ?? _currentTestName(); @@ -66,13 +68,18 @@ class MockServer { } static Future _get(String url) async { - final client = HttpClient()..connectionTimeout = _httpTimeout; - try { - final req = await client.getUrl(Uri.parse(url)); - final res = await req.close().timeout(_httpTimeout); - return res.transform(utf8.decoder).join().timeout(_httpTimeout); - } finally { - client.close(force: true); + for (var attempt = 1; ; attempt++) { + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + return await res.transform(utf8.decoder).join().timeout(_httpTimeout); + } on Exception { + if (attempt >= _retries) rethrow; + await Future.delayed(_retryBackoff * attempt); + } finally { + client.close(force: true); + } } } From 2af32e54279e756a3c2c61be2559aa8da46bb2e0 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 20:53:20 +0100 Subject: [PATCH 19/33] Fix --- .../mock_server/mock_server.dart | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index fc2c35a692..47c0f64708 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -17,8 +17,6 @@ class MockServer { static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); - static const _retries = 3; - static const _retryBackoff = Duration(milliseconds: 250); static Future start({String? testName}) async { final name = testName ?? _currentTestName(); @@ -68,18 +66,13 @@ class MockServer { } static Future _get(String url) async { - for (var attempt = 1; ; attempt++) { - final client = HttpClient()..connectionTimeout = _httpTimeout; - try { - final req = await client.getUrl(Uri.parse(url)); - final res = await req.close().timeout(_httpTimeout); - return await res.transform(utf8.decoder).join().timeout(_httpTimeout); - } on Exception { - if (attempt >= _retries) rethrow; - await Future.delayed(_retryBackoff * attempt); - } finally { - client.close(force: true); - } + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + return await res.transform(utf8.decoder).join().timeout(_httpTimeout); + } finally { + client.close(force: true); } } From 09f0fcbc2b79bc01a9a896fc44ca91a068efaa41 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 10 Jul 2026 21:35:04 +0100 Subject: [PATCH 20/33] Test --- .../mock_server/mock_server.dart | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index 47c0f64708..3407a04b61 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -12,16 +12,22 @@ class MockServer { 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 _driverPort = String.fromEnvironment('MOCK_DRIVER_PORT', defaultValue: '4568'); static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); + // `/start` is a one-shot, non-idempotent call that spawns a whole new server + // process on the driver. On a cold/loaded CI runner the driver can take many + // seconds just to begin servicing the request (it sits in the TCP backlog), + // so it needs a far more generous budget than a normal request β€” and it must + // NOT be retried (each call spawns another server on another port). + 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')).trim(); + final port = (await _get('$driverUrl/start/$name', timeout: _driverStartTimeout)).trim(); final server = MockServer._('http://$_host:$port', 'ws://$_host:$port'); await server.waitUntilReady(); return server; @@ -56,21 +62,22 @@ class MockServer { }) 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); + 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) async { - final client = HttpClient()..connectionTimeout = _httpTimeout; + 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(_httpTimeout); - return await res.transform(utf8.decoder).join().timeout(_httpTimeout); + final res = await req.close().timeout(timeout); + return await res.transform(utf8.decoder).join().timeout(timeout); } finally { client.close(force: true); } From 1d6f406fa8bd318e5695144400ed726be9046495 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 14:12:41 +0100 Subject: [PATCH 21/33] Test macos 15 --- .github/workflows/e2e_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 767e1b20a2..a9a63a8ffd 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -77,7 +77,7 @@ jobs: # sample_app/build/app/reports ios: - runs-on: macos-26 + runs-on: macos-15 env: COMPILER_INDEX_STORE_ENABLE: NO steps: From 284044c0d9556f6aa56c4515beb922f1bcde2e7d Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 14:51:46 +0100 Subject: [PATCH 22/33] Revert "Test" This reverts commit 09f0fcbc2b79bc01a9a896fc44ca91a068efaa41. --- .../mock_server/mock_server.dart | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index 3407a04b61..47c0f64708 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -12,22 +12,16 @@ class MockServer { 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 _driverPort = + String.fromEnvironment('MOCK_DRIVER_PORT', defaultValue: '4568'); static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); - // `/start` is a one-shot, non-idempotent call that spawns a whole new server - // process on the driver. On a cold/loaded CI runner the driver can take many - // seconds just to begin servicing the request (it sits in the TCP backlog), - // so it needs a far more generous budget than a normal request β€” and it must - // NOT be retried (each call spawns another server on another port). - 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 port = (await _get('$driverUrl/start/$name')).trim(); final server = MockServer._('http://$_host:$port', 'ws://$_host:$port'); await server.waitUntilReady(); return server; @@ -62,22 +56,21 @@ class MockServer { }) 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); + 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; + static Future _get(String url) async { + final client = HttpClient()..connectionTimeout = _httpTimeout; try { final req = await client.getUrl(Uri.parse(url)); - final res = await req.close().timeout(timeout); - return await res.transform(utf8.decoder).join().timeout(timeout); + final res = await req.close().timeout(_httpTimeout); + return await res.transform(utf8.decoder).join().timeout(_httpTimeout); } finally { client.close(force: true); } From d49978b04e0bf5cdff31f70d839e153712eb69b2 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 14:51:55 +0100 Subject: [PATCH 23/33] Revert "Fix" This reverts commit 2af32e54279e756a3c2c61be2559aa8da46bb2e0. --- .../mock_server/mock_server.dart | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index 47c0f64708..fc2c35a692 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -17,6 +17,8 @@ class MockServer { static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); + static const _retries = 3; + static const _retryBackoff = Duration(milliseconds: 250); static Future start({String? testName}) async { final name = testName ?? _currentTestName(); @@ -66,13 +68,18 @@ class MockServer { } static Future _get(String url) async { - final client = HttpClient()..connectionTimeout = _httpTimeout; - try { - final req = await client.getUrl(Uri.parse(url)); - final res = await req.close().timeout(_httpTimeout); - return await res.transform(utf8.decoder).join().timeout(_httpTimeout); - } finally { - client.close(force: true); + for (var attempt = 1; ; attempt++) { + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + return await res.transform(utf8.decoder).join().timeout(_httpTimeout); + } on Exception { + if (attempt >= _retries) rethrow; + await Future.delayed(_retryBackoff * attempt); + } finally { + client.close(force: true); + } } } From 0d09fe48b00d38ffe462a8c22f92507ce2f91d34 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 14:52:01 +0100 Subject: [PATCH 24/33] Revert "fix(e2e): await mock server response body before closing client" This reverts commit b28272bff4409ced2c16be789feb30202bed982e. --- .../mock_server/mock_server.dart | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index fc2c35a692..6f9565f4fa 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -17,8 +17,6 @@ class MockServer { static const _httpTimeout = Duration(seconds: 10); static const _pingTimeout = Duration(seconds: 2); - static const _retries = 3; - static const _retryBackoff = Duration(milliseconds: 250); static Future start({String? testName}) async { final name = testName ?? _currentTestName(); @@ -68,18 +66,13 @@ class MockServer { } static Future _get(String url) async { - for (var attempt = 1; ; attempt++) { - final client = HttpClient()..connectionTimeout = _httpTimeout; - try { - final req = await client.getUrl(Uri.parse(url)); - final res = await req.close().timeout(_httpTimeout); - return await res.transform(utf8.decoder).join().timeout(_httpTimeout); - } on Exception { - if (attempt >= _retries) rethrow; - await Future.delayed(_retryBackoff * attempt); - } finally { - client.close(force: true); - } + final client = HttpClient()..connectionTimeout = _httpTimeout; + try { + final req = await client.getUrl(Uri.parse(url)); + final res = await req.close().timeout(_httpTimeout); + return res.transform(utf8.decoder).join().timeout(_httpTimeout); + } finally { + client.close(force: true); } } From 160efd5f91018a0d3bd10e2e168793aa0849b8f9 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 16:44:23 +0100 Subject: [PATCH 25/33] Check if this makes build slower --- .github/workflows/e2e_test.yml | 8 ++------ .github/workflows/e2e_test_cron.yml | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index a9a63a8ffd..64374b153b 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -99,7 +99,7 @@ jobs: run: | flutter precache --ios flutter pub global activate melos - melos bootstrap --category=packages --category=sample_app + melos bootstrap dart pub global activate patrol_cli - name: Cache CocoaPods @@ -107,7 +107,7 @@ jobs: with: path: | sample_app/ios/Pods - ${{ env.HOME }}/Library/Caches/CocoaPods + ~/Library/Caches/CocoaPods key: pods-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ runner.os }}- @@ -120,10 +120,6 @@ jobs: patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}- patrol-ios-integ-${{ runner.os }}- - - name: Install CocoaPods - working-directory: sample_app/ios - run: pod install - - name: Boot simulator run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index e200185a38..acdea021c1 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -142,7 +142,7 @@ jobs: run: | flutter precache --ios flutter pub global activate melos - melos bootstrap --category=packages --category=sample_app + melos bootstrap dart pub global activate patrol_cli - uses: ./.github/actions/setup-ios-runtime @@ -157,7 +157,7 @@ jobs: with: path: | sample_app/ios/Pods - ${{ env.HOME }}/Library/Caches/CocoaPods + ~/Library/Caches/CocoaPods key: pods-${{ matrix.ios }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ matrix.ios }}- @@ -170,10 +170,6 @@ jobs: patrol-ios-integ-${{ runner.os }}-xcode-26.2- patrol-ios-integ-${{ runner.os }}- - - name: Install CocoaPods - working-directory: sample_app/ios - run: pod install - - name: Boot simulator run: | if [ "${{ matrix.setup_runtime }}" = "true" ]; then From 7807ee1cd127e464200f08df80e1f47099cba1b5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 17:35:20 +0100 Subject: [PATCH 26/33] Test without caching build --- .github/workflows/e2e_test.yml | 9 --------- .github/workflows/e2e_test_cron.yml | 9 --------- 2 files changed, 18 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 64374b153b..4afdff9c40 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -111,15 +111,6 @@ jobs: key: pods-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ runner.os }}- - - name: Cache Patrol iOS build - uses: actions/cache@v6 - with: - path: sample_app/build/ios_integ - key: patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}- - patrol-ios-integ-${{ runner.os }}- - - name: Boot simulator run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index acdea021c1..37efae14be 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -161,15 +161,6 @@ jobs: key: pods-${{ matrix.ios }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ matrix.ios }}- - - name: Cache Patrol iOS build - uses: actions/cache@v6 - with: - path: sample_app/build/ios_integ - key: patrol-ios-integ-${{ runner.os }}-xcode-26.2-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-ios-integ-${{ runner.os }}-xcode-26.2- - patrol-ios-integ-${{ runner.os }}- - - name: Boot simulator run: | if [ "${{ matrix.setup_runtime }}" = "true" ]; then From 040545bea281445f06467c5fbad686e7a910a140 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 20:07:49 +0100 Subject: [PATCH 27/33] Revert "Test without caching build" This reverts commit 7807ee1cd127e464200f08df80e1f47099cba1b5. --- .github/workflows/e2e_test.yml | 9 +++++++++ .github/workflows/e2e_test_cron.yml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 4afdff9c40..64374b153b 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -111,6 +111,15 @@ jobs: key: pods-${{ runner.os }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ runner.os }}- + - name: Cache Patrol iOS build + uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}- + patrol-ios-integ-${{ runner.os }}- + - name: Boot simulator run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 37efae14be..acdea021c1 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -161,6 +161,15 @@ jobs: key: pods-${{ matrix.ios }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} restore-keys: pods-${{ matrix.ios }}- + - name: Cache Patrol iOS build + uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-26.2-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-26.2- + patrol-ios-integ-${{ runner.os }}- + - name: Boot simulator run: | if [ "${{ matrix.setup_runtime }}" = "true" ]; then From 896af67700bc785ad4ad785de9144c6aae4683cc Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sat, 11 Jul 2026 21:41:07 +0100 Subject: [PATCH 28/33] test(e2e): retry failed tests --- sample_app/fastlane/Fastfile | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index bc6ac47167..0cedb10a1a 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -58,6 +58,29 @@ lane :stop_mock_server do Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil end +# Runs `command`, retrying up to `retries` more times after a failure (like +# iOS/Android native `scan(number_of_retries:)`). Returns true on success, false +# once the budget is exhausted. `label` names the unit in log messages. +private_lane :sh_with_retries do |options| + command = options[:command] + retries = (options[:retries] || 3).to_i + label = options[:label] || command + + attempt = 0 + begin + attempt += 1 + sh(command) + 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) @@ -73,16 +96,27 @@ lane :run_e2e_test do |options| sh('bash tool/patch_patrol_cli_android_gradle.sh') end - flags = [] - flags << "--target #{options[:target]}" if options[:target] - flags << "--device #{options[:device]}" if options[:device] - flags << '--verbose' if options[:verbose] + ios_version = nil if ios_device?(options[:device]) ios_version = options[:ios] || ENV['IOS_SIMULATOR_VERSION'] || ios_simulator_runtime_version(options[:device]) + end + + 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 #{target}"] + flags << "--device #{options[:device]}" if options[:device] + flags << '--verbose' if options[:verbose] flags << "--ios #{ios_version}" if ios_version && !ios_version.empty? + + ok = sh_with_retries(command: "patrol test #{flags.join(' ')}", retries: options[:retries], label: target) + failed << target unless ok end - sh("patrol test #{flags.join(' ')}".strip) + + UI.user_error!("E2E failed after retries: #{failed.join(', ')}") unless failed.empty? end ensure collect_allure_results(device: options[:device]) rescue nil From a7357efd53d75c6910010c466d434e22af0ef881 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sun, 12 Jul 2026 11:55:48 +0100 Subject: [PATCH 29/33] test(e2e): boot ios simulator via prepare_simulator in run_e2e_test --- .github/workflows/e2e_test.yml | 8 +--- .github/workflows/e2e_test_cron.yml | 12 +---- sample_app/fastlane/Fastfile | 28 +++++------ sample_app/tool/boot_ios_simulator.sh | 69 --------------------------- 4 files changed, 16 insertions(+), 101 deletions(-) delete mode 100755 sample_app/tool/boot_ios_simulator.sh diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 64374b153b..9124a32915 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -120,14 +120,8 @@ jobs: patrol-ios-integ-${{ runner.os }}-xcode-${{ env.ios_simulator_version }}- patrol-ios-integ-${{ runner.os }}- - - name: Boot simulator - run: bash sample_app/tool/boot_ios_simulator.sh "${{ env.ios_simulator_device }}" "${{ env.ios_simulator_version }}" - - name: Run e2e (iOS simulator) - env: - IOS_SIMULATOR_VERSION: ${{ env.ios_simulator_version }} - run: | - cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} + run: cd sample_app/ios && bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.ios_simulator_version }}" device:"${{ env.ios_simulator_device }}" - name: Upload Allure results if: always() diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index acdea021c1..6f6cfa045c 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -170,18 +170,8 @@ jobs: patrol-ios-integ-${{ runner.os }}-xcode-26.2- patrol-ios-integ-${{ runner.os }}- - - name: Boot simulator - run: | - if [ "${{ matrix.setup_runtime }}" = "true" ]; then - bash sample_app/tool/boot_ios_simulator.sh custom-test-device - else - bash sample_app/tool/boot_ios_simulator.sh "${{ matrix.device }}" "${{ matrix.ios }}" - fi - - name: Run e2e (iOS simulator) - env: - IOS_SIMULATOR_VERSION: ${{ matrix.ios }} - run: cd sample_app/ios && bundle exec fastlane run_e2e_test device:${{ env.device_id }} mock_server_branch:${{ env.mock_server_branch }} + run: cd sample_app/ios && 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: success() || failure() diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 0cedb10a1a..e339aa808a 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -84,23 +84,24 @@ 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]) + ios_version = nil + device = options[:device] + + if ios + device = prepare_simulator(device: options[:device], reset: true) + ios_version = options[:ios] || ios_simulator_runtime_version(device) + end + start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) - unless ios_device?(options[:device]) - serial = options[:device] ? "-s #{options[:device]} " : '' + unless ios + serial = device ? "-s #{device} " : '' sh("adb #{serial}logcat -c") end begin Dir.chdir(root_path) do - unless ios_device?(options[:device]) - sh('bash tool/patch_patrol_cli_android_gradle.sh') - end - - ios_version = nil - if ios_device?(options[:device]) - ios_version = options[:ios] || ENV['IOS_SIMULATOR_VERSION'] || - ios_simulator_runtime_version(options[:device]) - end + sh('bash tool/patch_patrol_cli_android_gradle.sh') unless ios targets = options[:target] ? [options[:target]] : Dir['integration_test/*_test.dart'].sort UI.user_error!('No e2e test files found') if targets.empty? @@ -108,7 +109,7 @@ lane :run_e2e_test do |options| failed = [] targets.each do |target| flags = ["--target #{target}"] - flags << "--device #{options[:device]}" if options[:device] + flags << "--device #{device}" if device flags << '--verbose' if options[:verbose] flags << "--ios #{ios_version}" if ios_version && !ios_version.empty? @@ -119,7 +120,7 @@ lane :run_e2e_test do |options| UI.user_error!("E2E failed after retries: #{failed.join(', ')}") unless failed.empty? end ensure - collect_allure_results(device: options[:device]) rescue nil + collect_allure_results(device: device) rescue nil stop_mock_server end end @@ -154,7 +155,6 @@ private_lane :sources_matrix do '.github/workflows/e2e_test.yml', '.github/workflows/e2e_test_cron.yml', 'sample_app/tool/patch_patrol_cli_android_gradle.sh', - 'sample_app/tool/boot_ios_simulator.sh', 'sample_app/tool/install_ios_runtime.sh', '.github/actions/setup-ios-runtime', ], diff --git a/sample_app/tool/boot_ios_simulator.sh b/sample_app/tool/boot_ios_simulator.sh deleted file mode 100755 index d6b952be6f..0000000000 --- a/sample_app/tool/boot_ios_simulator.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -DEVICE_NAME="${1:?usage: boot_ios_simulator.sh [ios-version]}" -IOS_VERSION="${2:-${IOS_SIMULATOR_VERSION:-}}" - -if [[ -z "${GITHUB_ENV:-}" ]]; then - echo "GITHUB_ENV must be set (CI step env)" >&2 - exit 1 -fi - -xcrun simctl shutdown all 2>/dev/null || true - -device_id="$( - xcrun simctl list devices available --json | python3 -c ' -import json -import sys - -target, ios_version = sys.argv[1], sys.argv[2] -data = json.load(sys.stdin) - -if ios_version: - runtime_slug = "iOS-" + ios_version.replace(".", "-") - for runtime, devices in data["devices"].items(): - if runtime_slug not in runtime: - continue - for device in devices: - if device["name"] == target and device.get("isAvailable", True): - print(device["udid"]) - sys.exit(0) -else: - for runtime, devices in data["devices"].items(): - if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"): - continue - for device in devices: - if device["name"] == target and device.get("isAvailable", True): - print(device["udid"]) - sys.exit(0) - -matches = [] -for runtime, devices in data["devices"].items(): - if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"): - continue - for device in devices: - if device.get("isAvailable", True): - name = device["name"] - os_tag = runtime.split("iOS-")[-1] - matches.append(f"{name} ({os_tag})") - -hint = "\n".join(sorted(matches)[:20]) if matches else "(none)" -version_hint = f" on iOS {ios_version}" if ios_version else "" -raise SystemExit( - f"No available simulator {target!r}{version_hint}\n" - f"Available iOS simulators:\n{hint}" -) -' "$DEVICE_NAME" "$IOS_VERSION" -)" - -echo "device_id=${device_id}" >>"$GITHUB_ENV" -xcrun simctl boot "${device_id}" 2>/dev/null || true -xcrun simctl bootstatus "${device_id}" -b - -sleep 10 - -if [[ -n "$IOS_VERSION" ]]; then - echo "Booted ${DEVICE_NAME} (${device_id}) on iOS ${IOS_VERSION}" -else - echo "Booted ${DEVICE_NAME} (${device_id})" -fi From 662753230bdacb2a4d0009b5c39c64900efa82af Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sun, 12 Jul 2026 15:52:29 +0100 Subject: [PATCH 30/33] fix(e2e): await /start response body and give it a 60s budget --- .../mock_server/mock_server.dart | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sample_app/integration_test/mock_server/mock_server.dart b/sample_app/integration_test/mock_server/mock_server.dart index 6f9565f4fa..bed03afecb 100644 --- a/sample_app/integration_test/mock_server/mock_server.dart +++ b/sample_app/integration_test/mock_server/mock_server.dart @@ -18,10 +18,16 @@ class MockServer { 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')).trim(); + final port = + (await _get('$driverUrl/start/$name', timeout: _driverStartTimeout)) + .trim(); final server = MockServer._('http://$_host:$port', 'ws://$_host:$port'); await server.waitUntilReady(); return server; @@ -65,12 +71,17 @@ class MockServer { throw StateError('Mock server at $url did not become ready in $timeout'); } - static Future _get(String url) async { - final client = HttpClient()..connectionTimeout = _httpTimeout; + 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(_httpTimeout); - return res.transform(utf8.decoder).join().timeout(_httpTimeout); + 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); } From 8ceed4bd657aeeb8676eadd55eee3fc3928a0ec1 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sun, 12 Jul 2026 16:54:46 +0100 Subject: [PATCH 31/33] Do not wait for ios sim --- sample_app/Gemfile.lock | 6 +++--- sample_app/fastlane/Fastfile | 2 +- sample_app/fastlane/Pluginfile | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sample_app/Gemfile.lock b/sample_app/Gemfile.lock index ccc26ac958..d1d247454e 100644 --- a/sample_app/Gemfile.lock +++ b/sample_app/Gemfile.lock @@ -192,7 +192,7 @@ GEM fastlane (>= 2.232.0) google-apis-firebaseappdistribution_v1 (>= 0.9.0) google-apis-firebaseappdistribution_v1alpha (>= 0.12.0) - fastlane-plugin-stream_actions (0.4.5) + fastlane-plugin-stream_actions (0.4.6) xctest_list (= 1.2.1) fastlane-sirp (1.1.0) faye-websocket (0.12.0) @@ -386,7 +386,7 @@ DEPENDENCIES fastlane fastlane-plugin-aws_s3 fastlane-plugin-firebase_app_distribution - fastlane-plugin-stream_actions + fastlane-plugin-stream_actions (= 0.4.6) faye-websocket multi_json puma @@ -454,7 +454,7 @@ CHECKSUMS 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.5) sha256=6b69093c2e1156b39414d1625ccabf8555a84005633f225708f591a3bcc38aa8 + 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 diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index e339aa808a..8b5df0b5dd 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -89,7 +89,7 @@ lane :run_e2e_test do |options| device = options[:device] if ios - device = prepare_simulator(device: options[:device], reset: true) + device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: false) ios_version = options[:ios] || ios_simulator_runtime_version(device) end diff --git a/sample_app/fastlane/Pluginfile b/sample_app/fastlane/Pluginfile index 3b1aeb89e6..27d5852144 100644 --- a/sample_app/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' From f2d3bf8cc9425abf42401c1c9f67978c8fd41e5b Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sun, 12 Jul 2026 18:54:50 +0100 Subject: [PATCH 32/33] Uncomment android --- .github/workflows/e2e_test.yml | 110 ++++++++++++++++----------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 9124a32915..48d0b023b2 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -20,61 +20,61 @@ env: ios_simulator_device: "iPhone 17" jobs: - # android: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v6 - - # - uses: actions/setup-java@v5 - # with: - # distribution: temurin - # java-version: "17" - - # - 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: ruby/setup-ruby@v1 - # with: - # ruby-version: "3.3" - # bundler-cache: true - # working-directory: sample_app - - # - name: Bootstrap - # run: | - # flutter pub global activate melos - # melos bootstrap - # dart pub global activate patrol_cli - - # - name: Enable KVM - # run: | - # echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - # sudo udevadm control --reload-rules - # sudo udevadm trigger --name-match=kvm - - # - name: Run e2e (Android emulator) - # uses: reactivecircus/android-emulator-runner@v2 - # with: - # api-level: 34 - # 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: always() - # env: - # ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} - # run: cd sample_app/android && bundle exec fastlane allure_upload - - # - uses: actions/upload-artifact@v7 - # if: always() - # with: - # name: e2e-android-logs - # path: | - # sample_app/stream-chat-test-mock-server/logs - # sample_app/build/app/reports + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + + - 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: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app + + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap + dart pub global activate patrol_cli + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + 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: always() + env: + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} + run: cd sample_app/android && bundle exec fastlane allure_upload + + - uses: actions/upload-artifact@v7 + if: always() + with: + name: e2e-android-logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/app/reports ios: runs-on: macos-15 From c3057484477cf8504497aaf6e1f71fb42d19c492 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Sun, 12 Jul 2026 22:45:50 +0100 Subject: [PATCH 33/33] Refactor android --- melos.yaml | 1 + sample_app/fastlane/Fastfile | 8 +- sample_app/pubspec.yaml | 3 + .../{tool => scripts}/install_ios_runtime.sh | 0 .../tool/patch_patrol_cli_android_gradle.sh | 135 ------------------ 5 files changed, 6 insertions(+), 141 deletions(-) rename sample_app/{tool => scripts}/install_ios_runtime.sh (100%) delete mode 100755 sample_app/tool/patch_patrol_cli_android_gradle.sh diff --git a/melos.yaml b/melos.yaml index ce7e8581c6..5b79e17fd0 100644 --- a/melos.yaml +++ b/melos.yaml @@ -83,6 +83,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 diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 8b5df0b5dd..dad92eaef6 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -101,8 +101,6 @@ lane :run_e2e_test do |options| begin Dir.chdir(root_path) do - sh('bash tool/patch_patrol_cli_android_gradle.sh') unless ios - targets = options[:target] ? [options[:target]] : Dir['integration_test/*_test.dart'].sort UI.user_error!('No e2e test files found') if targets.empty? @@ -129,7 +127,6 @@ lane :build_e2e_test do |options| next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check) Dir.chdir(root_path) do - sh('bash tool/patch_patrol_cli_android_gradle.sh') if options[:platform] == 'android' sh("patrol build #{options[:platform]}") end end @@ -142,7 +139,7 @@ end lane :install_runtime do |options| install_ios_runtime( version: options[:ios], - custom_script: File.join(root_path, 'tool/install_ios_runtime.sh'), + custom_script: File.join(root_path, 'scripts/install_ios_runtime.sh'), ) end @@ -154,8 +151,7 @@ private_lane :sources_matrix do 'melos.yaml', '.github/workflows/e2e_test.yml', '.github/workflows/e2e_test_cron.yml', - 'sample_app/tool/patch_patrol_cli_android_gradle.sh', - 'sample_app/tool/install_ios_runtime.sh', + 'sample_app/scripts/install_ios_runtime.sh', '.github/actions/setup-ios-runtime', ], ruby: ['sample_app/fastlane/Fastfile', 'sample_app/fastlane/Allurefile', 'sample_app/Gemfile', diff --git a/sample_app/pubspec.yaml b/sample_app/pubspec.yaml index 941688cf31..0da97999cc 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -39,6 +39,9 @@ dependencies: image_picker: ^1.2.2 latlong2: ^0.9.1 lottie: ^3.3.3 + # Keep the method-channel implementation: the jni-based 2.3.x breaks patrol's + # androidTest-only Gradle invocation on CI (flutter tasks are not registered). + 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 diff --git a/sample_app/tool/install_ios_runtime.sh b/sample_app/scripts/install_ios_runtime.sh similarity index 100% rename from sample_app/tool/install_ios_runtime.sh rename to sample_app/scripts/install_ios_runtime.sh diff --git a/sample_app/tool/patch_patrol_cli_android_gradle.sh b/sample_app/tool/patch_patrol_cli_android_gradle.sh deleted file mode 100755 index a6301b462c..0000000000 --- a/sample_app/tool/patch_patrol_cli_android_gradle.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PATROL_VERSION="${PATROL_CLI_VERSION:-$(dart pub global list 2>/dev/null | awk '/^patrol_cli / {print $2; exit}')}" -PATROL_VERSION="${PATROL_VERSION:-4.4.0}" -PUB_CACHE="${PUB_CACHE:-${HOME}/.pub-cache}" - -TARGET="${PUB_CACHE}/hosted/pub.dev/patrol_cli-${PATROL_VERSION}/lib/src/android/android_test_backend.dart" - -if [[ ! -f "${TARGET}" ]]; then - echo "patrol_cli source not found at ${TARGET}" >&2 - echo "Run: dart pub global activate patrol_cli" >&2 - exit 1 -fi - -if grep -q 'combinedInvocation' "${TARGET}"; then - echo "patrol_cli Android Gradle patch already applied" -else -python3 - "${TARGET}" <<'PY' -import sys - -path = sys.argv[1] -text = open(path, encoding="utf-8").read() - -old = """ // :app:assembleDebug - - process = - await _processManager.start( - options.toGradleAssembleInvocation( - isWindows: _platform.isWindows, - ), - runInShell: true, - workingDirectory: _rootDirectory.childDirectory('android').path, - environment: switch (javaPath) { - final String javaPath => {'JAVA_HOME': javaPath}, - _ => {}, - }, - ) - ..disposedBy(scope); - process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); - process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); - exitCode = await process.exitCode; - if (exitCode == exitCodeInterrupted) { - const cause = 'Gradle build interrupted'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - } else if (exitCode != 0) { - final cause = 'Gradle build failed with code $exitCode'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - } - - // :app:assembleDebugAndroidTest - - process = - await _processManager.start( - options.toGradleAssembleTestInvocation( - isWindows: _platform.isWindows, - ), - runInShell: true, - workingDirectory: _rootDirectory.childDirectory('android').path, - environment: switch (javaPath) { - final String javaPath => {'JAVA_HOME': javaPath}, - _ => {}, - }, - ) - ..disposedBy(scope); - process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); - process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); - - exitCode = await process.exitCode; - if (exitCode == 0) { - task.complete('Completed building $subject'); - } else if (exitCode == exitCodeInterrupted) { - const cause = 'Gradle build interrupted'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - } else { - final cause = 'Gradle build failed with code $exitCode'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - }""" - -new = """ // Patched by sample_app/tool/patch_patrol_cli_android_gradle.sh - - final assembleInvocation = options.toGradleAssembleInvocation( - isWindows: _platform.isWindows, - ); - final testInvocation = options.toGradleAssembleTestInvocation( - isWindows: _platform.isWindows, - ); - final combinedInvocation = [ - testInvocation.first, - assembleInvocation[1], - testInvocation[1], - ...testInvocation.skip(2), - ]; - - process = - await _processManager.start( - combinedInvocation, - runInShell: true, - workingDirectory: _rootDirectory.childDirectory('android').path, - environment: switch (javaPath) { - final String javaPath => {'JAVA_HOME': javaPath}, - _ => {}, - }, - ) - ..disposedBy(scope); - process.listenStdOut((l) => _logger.detail('\\t: $l')).disposedBy(scope); - process.listenStdErr((l) => _logger.err('\\t$l')).disposedBy(scope); - - exitCode = await process.exitCode; - if (exitCode == 0) { - task.complete('Completed building $subject'); - } else if (exitCode == exitCodeInterrupted) { - const cause = 'Gradle build interrupted'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - } else { - final cause = 'Gradle build failed with code $exitCode'; - task.fail('Failed to build $subject ($cause)'); - throw Exception(cause); - }""" - -if old not in text: - print("patrol_cli android_test_backend.dart changed; patch no longer applies", file=sys.stderr) - sys.exit(1) - -open(path, "w", encoding="utf-8").write(text.replace(old, new, 1)) -print(f"Patched {path}") -PY -fi - -rm -f "${PUB_CACHE}/global_packages/patrol_cli/bin/"*.snapshot 2>/dev/null || true