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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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/66] 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 From 712aa8e34678e7b5b94dd1a25849e6f36bceb05a Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 13:52:09 +0100 Subject: [PATCH 34/66] Clean up CI --- .github/actions/cache-cocoapods/action.yml | 16 +++ .../actions/cache-patrol-ios-build/action.yml | 16 +++ .github/actions/enable-kvm/action.yml | 11 ++ .github/actions/setup-flutter/action.yml | 10 ++ .github/actions/setup-java/action.yml | 9 ++ .github/actions/setup-ruby/action.yml | 10 ++ .github/workflows/distribute_external.yml | 34 +---- .github/workflows/distribute_internal.yml | 47 ++----- .github/workflows/e2e_test.yml | 88 +++++------- .github/workflows/e2e_test_cron.yml | 128 +++++------------- .github/workflows/stream_flutter_workflow.yml | 35 +---- sample_app/fastlane/Allurefile | 6 +- sample_app/fastlane/Fastfile | 9 +- 13 files changed, 169 insertions(+), 250 deletions(-) create mode 100644 .github/actions/cache-cocoapods/action.yml create mode 100644 .github/actions/cache-patrol-ios-build/action.yml create mode 100644 .github/actions/enable-kvm/action.yml create mode 100644 .github/actions/setup-flutter/action.yml create mode 100644 .github/actions/setup-java/action.yml create mode 100644 .github/actions/setup-ruby/action.yml diff --git a/.github/actions/cache-cocoapods/action.yml b/.github/actions/cache-cocoapods/action.yml new file mode 100644 index 0000000000..ad5316d759 --- /dev/null +++ b/.github/actions/cache-cocoapods/action.yml @@ -0,0 +1,16 @@ +name: 'Cache CocoaPods' +description: 'Cache CocoaPods dependencies for sample_app iOS builds' +inputs: + key-suffix: + description: 'Cache key suffix (e.g. runner.os or iOS version from matrix)' + required: true +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: | + sample_app/ios/Pods + ~/Library/Caches/CocoaPods + key: pods-${{ inputs.key-suffix }}-${{ hashFiles('pubspec.lock', 'sample_app/ios/Podfile') }} + restore-keys: pods-${{ inputs.key-suffix }}- diff --git a/.github/actions/cache-patrol-ios-build/action.yml b/.github/actions/cache-patrol-ios-build/action.yml new file mode 100644 index 0000000000..5182dd4dd9 --- /dev/null +++ b/.github/actions/cache-patrol-ios-build/action.yml @@ -0,0 +1,16 @@ +name: 'Cache Patrol iOS build' +description: 'Cache Patrol iOS integration test build artifacts' +inputs: + xcode-version: + description: 'Xcode / iOS simulator version used in the cache key' + required: true +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.xcode-version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.xcode-version }}- + patrol-ios-integ-${{ runner.os }}- diff --git a/.github/actions/enable-kvm/action.yml b/.github/actions/enable-kvm/action.yml new file mode 100644 index 0000000000..82c4148797 --- /dev/null +++ b/.github/actions/enable-kvm/action.yml @@ -0,0 +1,11 @@ +name: 'Enable KVM' +description: 'Configure KVM permissions for Android emulator on Linux runners' +runs: + using: "composite" + steps: + - name: Enable KVM + shell: bash + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm diff --git a/.github/actions/setup-flutter/action.yml b/.github/actions/setup-flutter/action.yml new file mode 100644 index 0000000000..318737252f --- /dev/null +++ b/.github/actions/setup-flutter/action.yml @@ -0,0 +1,10 @@ +name: 'Setup Flutter' +description: 'Install Flutter with dependency caching' +runs: + using: "composite" + steps: + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.x" + channel: stable + cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} diff --git a/.github/actions/setup-java/action.yml b/.github/actions/setup-java/action.yml new file mode 100644 index 0000000000..9ecd8e14a3 --- /dev/null +++ b/.github/actions/setup-java/action.yml @@ -0,0 +1,9 @@ +name: 'Setup Java' +description: 'Install Java' +runs: + using: "composite" + steps: + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" diff --git a/.github/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml new file mode 100644 index 0000000000..6bbccdabef --- /dev/null +++ b/.github/actions/setup-ruby/action.yml @@ -0,0 +1,10 @@ +name: 'Setup Ruby' +description: 'Install Ruby and Cache Gems' +runs: + using: "composite" + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + working-directory: sample_app diff --git a/.github/workflows/distribute_external.yml b/.github/workflows/distribute_external.yml index 429a15ccc6..3aafc53bfe 100644 --- a/.github/workflows/distribute_external.yml +++ b/.github/workflows/distribute_external.yml @@ -15,8 +15,6 @@ on: - ios - both -env: - FLUTTER_VERSION: '3.x' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -37,7 +35,7 @@ jobs: run: | # Is this a release? is_release="${{ github.event_name == 'release' }}" - + # If it's a release event, build both platforms if [[ "$is_release" == "true" ]]; then echo "Building all platforms due to release" @@ -45,11 +43,11 @@ jobs: echo "run_android=true" >> $GITHUB_OUTPUT exit 0 fi - + # For manual workflow dispatch, store platform in a variable platform="${{ github.event.inputs.platform }}" echo "Selected platform: $platform" - + # Set outputs only if they should be true [[ "$platform" == "ios" || "$platform" == "both" ]] && echo "run_ios=true" >> $GITHUB_OUTPUT || true [[ "$platform" == "android" || "$platform" == "both" ]] && echo "run_android=true" >> $GITHUB_OUTPUT || true @@ -69,18 +67,9 @@ jobs: with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Distribute to S3 working-directory: sample_app/android @@ -110,21 +99,12 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Distribute to TestFlight working-directory: sample_app/ios diff --git a/.github/workflows/distribute_internal.yml b/.github/workflows/distribute_internal.yml index 7adbcea4d4..3dc85cb928 100644 --- a/.github/workflows/distribute_internal.yml +++ b/.github/workflows/distribute_internal.yml @@ -18,8 +18,6 @@ on: - ios - both -env: - FLUTTER_VERSION: '3.x' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -40,7 +38,7 @@ jobs: run: | # Is this a branch push? is_branch_push="${{ github.event_name == 'push' }}" - + # If it's a branch push, build both platforms if [[ "$is_branch_push" == "true" ]]; then echo "Building all platforms due to branch push" @@ -48,11 +46,11 @@ jobs: echo "run_android=true" >> $GITHUB_OUTPUT exit 0 fi - + # For manual workflow dispatch, store platform in a variable platform="${{ github.event.inputs.platform }}" echo "Selected platform: $platform" - + # Set outputs only if they should be true [[ "$platform" == "ios" || "$platform" == "both" ]] && echo "run_ios=true" >> $GITHUB_OUTPUT || true [[ "$platform" == "android" || "$platform" == "both" ]] && echo "run_android=true" >> $GITHUB_OUTPUT || true @@ -73,12 +71,7 @@ jobs: with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: flutter pub global activate melos @@ -86,11 +79,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Setup Firebase Service Account' working-directory: sample_app/android @@ -120,12 +109,7 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager @@ -136,11 +120,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Setup Firebase Service Account working-directory: sample_app/ios @@ -173,12 +153,7 @@ jobs: with: xcode-version: '26.3' - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.FLUTTER_VERSION }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Disable Swift Package Manager" run: flutter config --no-enable-swift-package-manager @@ -189,11 +164,7 @@ jobs: - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Distribute to TestFlight Internal working-directory: sample_app/ios diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 48d0b023b2..109bc0ff7c 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -11,13 +11,13 @@ permissions: contents: read concurrency: - group: e2e-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - flutter_version: "3.x" ios_simulator_version: "26.2" ios_simulator_device: "iPhone 17" + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} jobs: android: @@ -25,22 +25,11 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: "17" + - uses: ./.github/actions/setup-java - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-ruby - - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-flutter - name: Bootstrap run: | @@ -48,11 +37,12 @@ jobs: 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 + - uses: ./.github/actions/enable-kvm + + - name: Launch Allure TestOps + run: bundle exec fastlane allure_launch + env: + GITHUB_EVENT: ${{ toJson(github.event) }} - name: Run e2e (Android emulator) uses: reactivecircus/android-emulator-runner@v2 @@ -63,13 +53,15 @@ jobs: 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 }} + if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) run: cd sample_app/android && bundle exec fastlane allure_upload + - name: Allure TestOps Launch Removal + if: env.ALLURE_LAUNCH_ID != '' && cancelled() + run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + - uses: actions/upload-artifact@v7 - if: always() + if: failure() with: name: e2e-android-logs path: | @@ -83,17 +75,9 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-ruby - name: Bootstrap run: | @@ -102,32 +86,30 @@ jobs: melos bootstrap dart pub global activate patrol_cli - - name: Cache CocoaPods - uses: actions/cache@v6 + - uses: ./.github/actions/cache-cocoapods 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 }}- + key-suffix: ${{ runner.os }} - - name: Cache Patrol iOS build - uses: actions/cache@v6 + - uses: ./.github/actions/cache-patrol-ios-build 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 }}- + xcode-version: ${{ env.ios_simulator_version }} - name: Run e2e (iOS simulator) 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() - env: - ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} - run: cd sample_app/ios && bundle exec fastlane allure_upload + if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) + run: cd sample_app/android && bundle exec fastlane allure_upload + + - name: Allure TestOps Launch Removal + if: env.ALLURE_LAUNCH_ID != '' && cancelled() + run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + + - name: Parse xcresult + if: failure() + run: | + brew install chargepoint/xcparse/xcparse + xcparse logs sample_app/build/ios_integ/StreamChatUITestsApp.xcresult sample_app/build/ios_integ/logs/ - uses: actions/upload-artifact@v7 if: always() diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 6f6cfa045c..9c12aa6ae7 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -15,59 +15,28 @@ permissions: contents: read concurrency: - group: e2e-nightly-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - flutter_version: "3.x" mock_server_branch: ${{ github.event.inputs.mock_server_branch || 'main' }} + ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} 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 - - 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@v5 - with: - distribution: temurin - java-version: "17" + - uses: ./.github/actions/setup-java - - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-ruby - - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-flutter - name: Bootstrap run: | @@ -75,11 +44,10 @@ jobs: 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 + - uses: ./.github/actions/enable-kvm + + - name: Launch Allure TestOps + run: bundle exec fastlane allure_launch cron:true - name: Run e2e (Android emulator) uses: reactivecircus/android-emulator-runner@v2 @@ -92,11 +60,13 @@ jobs: 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 }} + if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) run: cd sample_app/android && bundle exec fastlane allure_upload + - name: Allure TestOps Launch Removal + if: env.ALLURE_LAUNCH_ID != '' && cancelled() + run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + - uses: actions/upload-artifact@v7 if: failure() with: @@ -104,7 +74,6 @@ jobs: path: sample_app/stream-chat-test-mock-server/logs ios: - needs: setup runs-on: macos-15 strategy: fail-fast: false @@ -121,22 +90,13 @@ jobs: 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: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-ruby - - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - bundler-cache: true - working-directory: sample_app + - uses: ./.github/actions/setup-flutter - name: Bootstrap run: | @@ -152,32 +112,33 @@ jobs: version: ${{ matrix.ios }} device: ${{ matrix.device }} - - name: Cache CocoaPods - uses: actions/cache@v6 + - uses: ./.github/actions/cache-cocoapods 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: Cache Patrol iOS build - uses: actions/cache@v6 + key-suffix: ${{ matrix.ios }} + + - uses: ./.github/actions/cache-patrol-ios-build 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 }}- + xcode-version: "26.2" + + - name: Launch Allure TestOps + run: bundle exec fastlane allure_launch cron:true - name: Run e2e (iOS simulator) 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() - env: - ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} - run: cd sample_app/ios && bundle exec fastlane allure_upload + if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) + run: cd sample_app/android && bundle exec fastlane allure_upload + + - name: Allure TestOps Launch Removal + if: env.ALLURE_LAUNCH_ID != '' && cancelled() + run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + + - name: Parse xcresult + if: failure() + run: | + brew install chargepoint/xcparse/xcparse + xcparse logs sample_app/build/ios_integ/StreamChatUITestsApp.xcresult sample_app/build/ios_integ/logs/ - uses: actions/upload-artifact@v7 if: failure() @@ -185,23 +146,6 @@ jobs: 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 - - 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 diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index f1f272bf4c..2e40d80fb8 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,7 +2,6 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "3.x" on: pull_request: @@ -62,12 +61,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: | flutter pub global activate melos @@ -91,12 +85,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - name: "Install Tools" run: | flutter pub global activate melos @@ -118,12 +107,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter # This step is needed due to https://github.com/actions/runner-images/issues/11279 - name: Install SQLite3 run: sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev @@ -192,12 +176,7 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 - - name: "Install Flutter" - uses: subosito/flutter-action@v2 - with: - flutter-version: ${{ env.flutter_version }} - channel: stable - cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: ./.github/actions/setup-flutter - uses: maxim-lobanov/setup-xcode@v1 if: matrix.platform == 'ios' with: @@ -206,11 +185,7 @@ jobs: run: flutter pub global activate melos - name: "Bootstrap Workspace" run: melos bootstrap - - name: Setup Ruby and Gems - uses: ruby/setup-ruby@v1 - with: - working-directory: sample_app - bundler-cache: true + - uses: ./.github/actions/setup-ruby - name: "Build ${{ matrix.platform }} App" working-directory: ${{ matrix.working-directory }} run: ${{ matrix.build-command }} diff --git a/sample_app/fastlane/Allurefile b/sample_app/fastlane/Allurefile index bef07dfc13..90ad8a2f95 100644 --- a/sample_app/fastlane/Allurefile +++ b/sample_app/fastlane/Allurefile @@ -49,8 +49,7 @@ lane :allure_upload do |options| 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}") + 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 @@ -69,6 +68,5 @@ lane :allure_launch_removal do |options| next unless id allurectl = install_allurectl - sh("ALLURE_ENDPOINT=#{allure_endpoint} ALLURE_PROJECT_ID=#{allure_project_id} " \ - "#{allurectl} launch delete #{id}") + sh("ALLURE_ENDPOINT=#{allure_endpoint} ALLURE_PROJECT_ID=#{allure_project_id} #{allurectl} launch delete #{id}") end diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index dad92eaef6..6478b27bef 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -7,7 +7,7 @@ import File.expand_path('Allurefile', __dir__) mock_server_repo_name = 'stream-chat-test-mock-server' mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4568' -github_org = (ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-flutter').split('/').first +github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-swift' @force_check = false def root_path @@ -44,7 +44,7 @@ lane :start_mock_server do |options| 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") + sh("git clone -b #{branch} https://github.com/#{github_repo.split('/').first}/#{mock_server_repo}.git") end Dir.chdir(mock_server_repo) do @@ -58,12 +58,9 @@ 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 + retries = (options[:retries] || 2).to_i label = options[:label] || command attempt = 0 From 593bb0b414bb4f4fea4c983155007938dc1329dc Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 13:55:49 +0100 Subject: [PATCH 35/66] Cache android too --- .github/actions/cache-patrol-build/action.yml | 29 +++++++++++++++++++ .../actions/cache-patrol-ios-build/action.yml | 16 ---------- .github/workflows/e2e_test.yml | 13 +++++++-- .github/workflows/e2e_test_cron.yml | 10 +++++-- 4 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 .github/actions/cache-patrol-build/action.yml delete mode 100644 .github/actions/cache-patrol-ios-build/action.yml diff --git a/.github/actions/cache-patrol-build/action.yml b/.github/actions/cache-patrol-build/action.yml new file mode 100644 index 0000000000..170998fa99 --- /dev/null +++ b/.github/actions/cache-patrol-build/action.yml @@ -0,0 +1,29 @@ +name: 'Cache Patrol build' +description: 'Cache Patrol integration test build artifacts' +inputs: + platform: + description: 'Target platform (ios or android)' + required: true + version: + description: 'Xcode version (ios) or Android API level used in the cache key' + required: true +runs: + using: "composite" + steps: + - if: inputs.platform == 'ios' + uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}- + patrol-ios-integ-${{ runner.os }}- + + - if: inputs.platform == 'android' + uses: actions/cache@v6 + with: + path: sample_app/build/app + key: patrol-android-integ-${{ runner.os }}-api-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/android/app/build.gradle', 'sample_app/android/build.gradle', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-android-integ-${{ runner.os }}-api-${{ inputs.version }}- + patrol-android-integ-${{ runner.os }}- diff --git a/.github/actions/cache-patrol-ios-build/action.yml b/.github/actions/cache-patrol-ios-build/action.yml deleted file mode 100644 index 5182dd4dd9..0000000000 --- a/.github/actions/cache-patrol-ios-build/action.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: 'Cache Patrol iOS build' -description: 'Cache Patrol iOS integration test build artifacts' -inputs: - xcode-version: - description: 'Xcode / iOS simulator version used in the cache key' - required: true -runs: - using: "composite" - steps: - - uses: actions/cache@v6 - with: - path: sample_app/build/ios_integ - key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.xcode-version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.xcode-version }}- - patrol-ios-integ-${{ runner.os }}- diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 109bc0ff7c..62f86170d3 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -15,6 +15,7 @@ concurrency: cancel-in-progress: true env: + android_api_level: "34" ios_simulator_version: "26.2" ios_simulator_device: "iPhone 17" ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} @@ -39,6 +40,11 @@ jobs: - uses: ./.github/actions/enable-kvm + - uses: ./.github/actions/cache-patrol-build + with: + platform: android + version: ${{ env.android_api_level }} + - name: Launch Allure TestOps run: bundle exec fastlane allure_launch env: @@ -47,7 +53,7 @@ jobs: - name: Run e2e (Android emulator) uses: reactivecircus/android-emulator-runner@v2 with: - api-level: 34 + api-level: ${{ env.android_api_level }} arch: x86_64 profile: pixel_6 script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 @@ -90,9 +96,10 @@ jobs: with: key-suffix: ${{ runner.os }} - - uses: ./.github/actions/cache-patrol-ios-build + - uses: ./.github/actions/cache-patrol-build with: - xcode-version: ${{ env.ios_simulator_version }} + platform: ios + version: ${{ env.ios_simulator_version }} - name: Run e2e (iOS simulator) run: cd sample_app/ios && bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.ios_simulator_version }}" device:"${{ env.ios_simulator_device }}" diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 9c12aa6ae7..943cb401fb 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -46,6 +46,11 @@ jobs: - uses: ./.github/actions/enable-kvm + - uses: ./.github/actions/cache-patrol-build + with: + platform: android + version: ${{ matrix.api-level }} + - name: Launch Allure TestOps run: bundle exec fastlane allure_launch cron:true @@ -116,9 +121,10 @@ jobs: with: key-suffix: ${{ matrix.ios }} - - uses: ./.github/actions/cache-patrol-ios-build + - uses: ./.github/actions/cache-patrol-build with: - xcode-version: "26.2" + platform: ios + version: "26.2" - name: Launch Allure TestOps run: bundle exec fastlane allure_launch cron:true From 7b3cc18e8f0b18022313447be54896b0f581427a Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 14:03:30 +0100 Subject: [PATCH 36/66] Fix allure --- .github/workflows/e2e_test.yml | 13 +++++++++---- .github/workflows/e2e_test_cron.yml | 10 +++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 62f86170d3..dbfaf4870f 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -46,7 +46,7 @@ jobs: version: ${{ env.android_api_level }} - name: Launch Allure TestOps - run: bundle exec fastlane allure_launch + run: cd sample_app/android && bundle exec fastlane allure_launch env: GITHUB_EVENT: ${{ toJson(github.event) }} @@ -64,7 +64,7 @@ jobs: - name: Allure TestOps Launch Removal if: env.ALLURE_LAUNCH_ID != '' && cancelled() - run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - uses: actions/upload-artifact@v7 if: failure() @@ -101,16 +101,21 @@ jobs: platform: ios version: ${{ env.ios_simulator_version }} + - name: Launch Allure TestOps + run: cd sample_app/ios && bundle exec fastlane allure_launch + env: + GITHUB_EVENT: ${{ toJson(github.event) }} + - name: Run e2e (iOS simulator) 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: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) - run: cd sample_app/android && bundle exec fastlane allure_upload + run: cd sample_app/ios && bundle exec fastlane allure_upload - name: Allure TestOps Launch Removal if: env.ALLURE_LAUNCH_ID != '' && cancelled() - run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + run: cd sample_app/ios && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - name: Parse xcresult if: failure() diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 943cb401fb..8a177c494d 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -52,7 +52,7 @@ jobs: version: ${{ matrix.api-level }} - name: Launch Allure TestOps - run: bundle exec fastlane allure_launch cron:true + run: cd sample_app/android && bundle exec fastlane allure_launch cron:true - name: Run e2e (Android emulator) uses: reactivecircus/android-emulator-runner@v2 @@ -70,7 +70,7 @@ jobs: - name: Allure TestOps Launch Removal if: env.ALLURE_LAUNCH_ID != '' && cancelled() - run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - uses: actions/upload-artifact@v7 if: failure() @@ -127,18 +127,18 @@ jobs: version: "26.2" - name: Launch Allure TestOps - run: bundle exec fastlane allure_launch cron:true + run: cd sample_app/ios && bundle exec fastlane allure_launch cron:true - name: Run e2e (iOS simulator) 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: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) - run: cd sample_app/android && bundle exec fastlane allure_upload + run: cd sample_app/ios && bundle exec fastlane allure_upload - name: Allure TestOps Launch Removal if: env.ALLURE_LAUNCH_ID != '' && cancelled() - run: bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + run: cd sample_app/ios && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - name: Parse xcresult if: failure() From 98543a65e92b82c52122b24c712019ff14de8df7 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 14:48:41 +0100 Subject: [PATCH 37/66] Update caches --- .github/actions/cache-gradle/action.yml | 13 +++++++++ .../actions/cache-ios-patrol-build/action.yml | 16 ++++++++++ .github/actions/cache-patrol-build/action.yml | 29 ------------------- .github/workflows/e2e_test.yml | 10 ++----- .github/workflows/e2e_test_cron.yml | 10 ++----- 5 files changed, 35 insertions(+), 43 deletions(-) create mode 100644 .github/actions/cache-gradle/action.yml create mode 100644 .github/actions/cache-ios-patrol-build/action.yml delete mode 100644 .github/actions/cache-patrol-build/action.yml diff --git a/.github/actions/cache-gradle/action.yml b/.github/actions/cache-gradle/action.yml new file mode 100644 index 0000000000..3a70f83236 --- /dev/null +++ b/.github/actions/cache-gradle/action.yml @@ -0,0 +1,13 @@ +name: 'Cache Gradle' +description: 'Cache Gradle dependencies for sample_app Android builds' +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + sample_app/android/.gradle + key: gradle-${{ runner.os }}-${{ hashFiles('sample_app/android/gradle/wrapper/gradle-wrapper.properties', 'sample_app/android/app/build.gradle', 'sample_app/android/build.gradle', 'sample_app/android/settings.gradle', '**/pubspec.lock') }} + restore-keys: gradle-${{ runner.os }}- diff --git a/.github/actions/cache-ios-patrol-build/action.yml b/.github/actions/cache-ios-patrol-build/action.yml new file mode 100644 index 0000000000..72e891207f --- /dev/null +++ b/.github/actions/cache-ios-patrol-build/action.yml @@ -0,0 +1,16 @@ +name: 'Cache iOS Patrol build' +description: 'Cache Patrol iOS integration test build artifacts' +inputs: + version: + description: 'Xcode / iOS simulator version used in the cache key' + required: true +runs: + using: "composite" + steps: + - uses: actions/cache@v6 + with: + path: sample_app/build/ios_integ + key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} + restore-keys: | + patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}- + patrol-ios-integ-${{ runner.os }}- diff --git a/.github/actions/cache-patrol-build/action.yml b/.github/actions/cache-patrol-build/action.yml deleted file mode 100644 index 170998fa99..0000000000 --- a/.github/actions/cache-patrol-build/action.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: 'Cache Patrol build' -description: 'Cache Patrol integration test build artifacts' -inputs: - platform: - description: 'Target platform (ios or android)' - required: true - version: - description: 'Xcode version (ios) or Android API level used in the cache key' - required: true -runs: - using: "composite" - steps: - - if: inputs.platform == 'ios' - uses: actions/cache@v6 - with: - path: sample_app/build/ios_integ - key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}- - patrol-ios-integ-${{ runner.os }}- - - - if: inputs.platform == 'android' - uses: actions/cache@v6 - with: - path: sample_app/build/app - key: patrol-android-integ-${{ runner.os }}-api-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/android/app/build.gradle', 'sample_app/android/build.gradle', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-android-integ-${{ runner.os }}-api-${{ inputs.version }}- - patrol-android-integ-${{ runner.os }}- diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index dbfaf4870f..ffed73dfc1 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -28,6 +28,8 @@ jobs: - uses: ./.github/actions/setup-java + - uses: ./.github/actions/cache-gradle + - uses: ./.github/actions/setup-ruby - uses: ./.github/actions/setup-flutter @@ -40,11 +42,6 @@ jobs: - uses: ./.github/actions/enable-kvm - - uses: ./.github/actions/cache-patrol-build - with: - platform: android - version: ${{ env.android_api_level }} - - name: Launch Allure TestOps run: cd sample_app/android && bundle exec fastlane allure_launch env: @@ -96,9 +93,8 @@ jobs: with: key-suffix: ${{ runner.os }} - - uses: ./.github/actions/cache-patrol-build + - uses: ./.github/actions/cache-ios-patrol-build with: - platform: ios version: ${{ env.ios_simulator_version }} - name: Launch Allure TestOps diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 8a177c494d..4b2500fe3c 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -34,6 +34,8 @@ jobs: - uses: ./.github/actions/setup-java + - uses: ./.github/actions/cache-gradle + - uses: ./.github/actions/setup-ruby - uses: ./.github/actions/setup-flutter @@ -46,11 +48,6 @@ jobs: - uses: ./.github/actions/enable-kvm - - uses: ./.github/actions/cache-patrol-build - with: - platform: android - version: ${{ matrix.api-level }} - - name: Launch Allure TestOps run: cd sample_app/android && bundle exec fastlane allure_launch cron:true @@ -121,9 +118,8 @@ jobs: with: key-suffix: ${{ matrix.ios }} - - uses: ./.github/actions/cache-patrol-build + - uses: ./.github/actions/cache-ios-patrol-build with: - platform: ios version: "26.2" - name: Launch Allure TestOps From 25fe8b6113e848069bdd9ff81dc1d2f3c0caed57 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 15:12:59 +0100 Subject: [PATCH 38/66] Bump firebase --- melos.yaml | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 20 +++++++++---------- sample_app/pubspec.yaml | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/melos.yaml b/melos.yaml index 5b79e17fd0..4c93e7d398 100644 --- a/melos.yaml +++ b/melos.yaml @@ -47,8 +47,8 @@ command: equatable: ^2.0.8 ezanimation: ^0.6.0 firebase_core: ^4.0.0 - firebase_crashlytics: ^5.2.0 - firebase_messaging: ^16.0.0 + firebase_crashlytics: ">=5.2.0 <5.2.5" + firebase_messaging: ">=16.0.0 <16.4.2" file_picker: ^11.0.0 file_selector: ^1.1.0 flutter_app_badger: ^1.5.0 diff --git a/sample_app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sample_app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 521c8a269d..618518417f 100644 --- a/sample_app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sample_app/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "revision" : "3e33dd27dd4c69bd81c7c81fe61d8ccf58846902", + "version" : "11.3.1" } }, { @@ -58,8 +58,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" + "revision" : "42e81d245e30e49ea6a5830cf2842d44a1591270", + "version" : "12.15.0" } }, { @@ -67,8 +67,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", "state" : { - "revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64", - "version" : "3.6.0" + "revision" : "dc39082d8881109d35b94b1c122164c0e8d08a55", + "version" : "3.6.1" } }, { @@ -76,8 +76,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" + "revision" : "144855f40d8668927f256a3045f7fdc4c3f4338b", + "version" : "12.15.0" } }, { @@ -94,8 +94,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" + "revision" : "9f183ae842be978784f2963a343682e0c46d8fb3", + "version" : "8.1.2" } }, { diff --git a/sample_app/pubspec.yaml b/sample_app/pubspec.yaml index 0da97999cc..67364a0de0 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: avatar_glow: ^3.0.0 collection: ^1.19.1 firebase_core: ^4.0.0 - firebase_crashlytics: ^5.2.0 - firebase_messaging: ^16.0.0 + firebase_crashlytics: ">=5.2.0 <5.2.5" + firebase_messaging: ">=16.0.0 <16.4.2" flutter: sdk: flutter flutter_app_badger: ^1.5.0 From 2bb2140fe09569e9be2cc7f92418029a832478f4 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 16:10:59 +0100 Subject: [PATCH 39/66] Test ios --- .github/workflows/e2e_test.yml | 98 +++++++++++++++++----------------- sample_app/fastlane/Fastfile | 2 +- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index ffed73dfc1..1303412a9f 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -21,55 +21,55 @@ env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} jobs: - android: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: ./.github/actions/setup-java - - - uses: ./.github/actions/cache-gradle - - - uses: ./.github/actions/setup-ruby - - - uses: ./.github/actions/setup-flutter - - - name: Bootstrap - run: | - flutter pub global activate melos - melos bootstrap - dart pub global activate patrol_cli - - - uses: ./.github/actions/enable-kvm - - - name: Launch Allure TestOps - run: cd sample_app/android && bundle exec fastlane allure_launch - env: - GITHUB_EVENT: ${{ toJson(github.event) }} - - - name: Run e2e (Android emulator) - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ env.android_api_level }} - arch: x86_64 - profile: pixel_6 - script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 - - - name: Upload Allure results - if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) - run: cd sample_app/android && bundle exec fastlane allure_upload - - - name: Allure TestOps Launch Removal - if: env.ALLURE_LAUNCH_ID != '' && cancelled() - run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - - - uses: actions/upload-artifact@v7 - if: failure() - with: - name: e2e-android-logs - path: | - sample_app/stream-chat-test-mock-server/logs - sample_app/build/app/reports + # android: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + + # - uses: ./.github/actions/setup-java + + # - uses: ./.github/actions/cache-gradle + + # - uses: ./.github/actions/setup-ruby + + # - uses: ./.github/actions/setup-flutter + + # - name: Bootstrap + # run: | + # flutter pub global activate melos + # melos bootstrap + # dart pub global activate patrol_cli + + # - uses: ./.github/actions/enable-kvm + + # - name: Launch Allure TestOps + # run: cd sample_app/android && bundle exec fastlane allure_launch + # env: + # GITHUB_EVENT: ${{ toJson(github.event) }} + + # - name: Run e2e (Android emulator) + # uses: reactivecircus/android-emulator-runner@v2 + # with: + # api-level: ${{ env.android_api_level }} + # arch: x86_64 + # profile: pixel_6 + # script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 + + # - name: Upload Allure results + # if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) + # run: cd sample_app/android && bundle exec fastlane allure_upload + + # - name: Allure TestOps Launch Removal + # if: env.ALLURE_LAUNCH_ID != '' && cancelled() + # run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + + # - uses: actions/upload-artifact@v7 + # if: failure() + # with: + # name: e2e-android-logs + # path: | + # sample_app/stream-chat-test-mock-server/logs + # sample_app/build/app/reports ios: runs-on: macos-15 diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 6478b27bef..23e13b6c2b 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -86,7 +86,7 @@ lane :run_e2e_test do |options| device = options[:device] if ios - device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: false) + device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: true) ios_version = options[:ios] || ios_simulator_runtime_version(device) end From c2027ddf700348e7e2db89c497830b422e4357b0 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:30:48 +0100 Subject: [PATCH 40/66] Fix --- .github/actions/pana/action.yml | 4 ++-- sample_app/fastlane/Fastfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/pana/action.yml b/.github/actions/pana/action.yml index 1731910d8b..22b2091346 100644 --- a/.github/actions/pana/action.yml +++ b/.github/actions/pana/action.yml @@ -1,5 +1,5 @@ name: Pana Workflow - +description: 'Run pana package analysis and verify the score meets the minimum threshold' inputs: min_score: required: false @@ -28,7 +28,7 @@ runs: yq eval '.dependencies.stream_chat = {"path": "../stream_chat"}' -i packages/stream_chat_flutter_core/pubspec.yaml yq eval '.dependencies.stream_chat_flutter_core = {"path": "../stream_chat_flutter_core"}' -i packages/stream_chat_flutter/pubspec.yaml yq eval '.dependencies.stream_chat = {"path": "../stream_chat"}' -i packages/stream_chat_persistence/pubspec.yaml - + - name: Install Flutter uses: subosito/flutter-action@v2 with: diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 23e13b6c2b..6478b27bef 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -86,7 +86,7 @@ lane :run_e2e_test do |options| device = options[:device] if ios - device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: true) + device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: false) ios_version = options[:ios] || ios_simulator_runtime_version(device) end From 8f536324467a7fc93a047239d1866dc5532a0cef Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:30:54 +0100 Subject: [PATCH 41/66] iOS verbose log --- .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 1303412a9f..f99ffcc083 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -103,7 +103,7 @@ jobs: GITHUB_EVENT: ${{ toJson(github.event) }} - name: Run e2e (iOS simulator) - run: cd sample_app/ios && bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.ios_simulator_version }}" device:"${{ env.ios_simulator_device }}" + run: cd sample_app/ios && bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.ios_simulator_version }}" device:"${{ env.ios_simulator_device }}" verbose:true - name: Upload Allure results if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) From 3933981dcc8afa42b0264d3379faba520f6c0803 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:43:34 +0100 Subject: [PATCH 42/66] Fix --- .gitignore | 1 + .../xcshareddata/swiftpm/Package.resolved | 184 ------------------ 2 files changed, 1 insertion(+), 184 deletions(-) delete mode 100644 sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/.gitignore b/.gitignore index 7423d30d35..39580332dc 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,7 @@ GeneratedPluginRegistrant.* **/ios/Runner/GeneratedPluginRegistrant.* **/ios/Flutter/ephemeral/** Podfile.lock +**/project.xcworkspace/xcshareddata/swiftpm/Package.resolved # Exceptions to iOS rules !**/ios/**/default.mode1v3 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 deleted file mode 100644 index 521c8a269d..0000000000 --- a/sample_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,184 +0,0 @@ -{ - "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 -} From ab4a6afb975a800f279ca96038770f5fc757d218 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:44:06 +0100 Subject: [PATCH 43/66] Update patrol build phase --- .../ios/Runner.xcodeproj/project.pbxproj | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 454a0220c2..3f3a8f712b 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -218,6 +218,7 @@ C1289C2FFEC46190EEF8C3CE /* Frameworks */, 3DCFC2F9E1881E1CDF31C6A9 /* Resources */, BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */, + 8A056BE830054ACE00735E79 /* ShellScript */, ); buildRules = ( ); @@ -281,7 +282,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -352,7 +353,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -384,6 +385,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 8A056BE830054ACE00735E79 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -397,7 +415,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -852,7 +870,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; From f7adaa48dbbc9e343e8a1b1f9b8bed74fd387e4e Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:54:28 +0100 Subject: [PATCH 44/66] fix --- .github/workflows/e2e_test_cron.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index 4b2500fe3c..fa584f75b8 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -120,7 +120,7 @@ jobs: - uses: ./.github/actions/cache-ios-patrol-build with: - version: "26.2" + version: ${{ env.XCODE_VERSION }} - name: Launch Allure TestOps run: cd sample_app/ios && bundle exec fastlane allure_launch cron:true From cb92173a860de2f66e170c1dd899056495215738 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 17:54:45 +0100 Subject: [PATCH 45/66] Revert "Update patrol build phase" This reverts commit ab4a6afb975a800f279ca96038770f5fc757d218. --- .../ios/Runner.xcodeproj/project.pbxproj | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 3f3a8f712b..454a0220c2 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -218,7 +218,6 @@ C1289C2FFEC46190EEF8C3CE /* Frameworks */, 3DCFC2F9E1881E1CDF31C6A9 /* Resources */, BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */, - 8A056BE830054ACE00735E79 /* ShellScript */, ); buildRules = ( ); @@ -282,7 +281,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -353,7 +352,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 7BD3737C65F59B233B7F7FC6 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -385,23 +384,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8A056BE830054ACE00735E79 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -415,7 +397,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -870,7 +852,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; From ba559562531ef37d924eab47e93f0a7c54b4f683 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 18:04:56 +0100 Subject: [PATCH 46/66] Test ios --- sample_app/fastlane/Fastfile | 3 +- .../ios/Runner.xcodeproj/project.pbxproj | 3 ++ sample_app/ios/add_patrol_target.rb | 51 ------------------- 3 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 sample_app/ios/add_patrol_target.rb diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 6478b27bef..e8e9efe3cc 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -88,6 +88,7 @@ lane :run_e2e_test do |options| if ios device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: false) ios_version = options[:ios] || ios_simulator_runtime_version(device) + build_e2e_test(platform: 'ios') unless options[:skip_build] end start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) @@ -129,7 +130,7 @@ lane :build_e2e_test do |options| end lane :build_and_run_e2e_test do |options| - build_e2e_test(platform: options[:platform]) + build_e2e_test(platform: options[:platform]) unless options[:platform] == 'ios' run_e2e_test(options) end diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 454a0220c2..248d4b4798 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -511,6 +511,7 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_STYLE = Automatic; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; @@ -614,6 +615,7 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_STYLE = Automatic; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; @@ -633,6 +635,7 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = NO; CODE_SIGN_STYLE = Automatic; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter.RunnerUITests; diff --git a/sample_app/ios/add_patrol_target.rb b/sample_app/ios/add_patrol_target.rb deleted file mode 100644 index afc881570f..0000000000 --- a/sample_app/ios/add_patrol_target.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'xcodeproj' - -project_path = 'Runner.xcodeproj' -project = Xcodeproj::Project.open(project_path) - -if project.targets.any? { |t| t.name == 'RunnerUITests' } - puts 'RunnerUITests target already exists; nothing to do.' - exit 0 -end - -runner = project.targets.find { |t| t.name == 'Runner' } -raise 'Runner target not found' unless runner - -deployment_target = runner.build_configurations.first - .build_settings['IPHONEOS_DEPLOYMENT_TARGET'] || '15.0' - -test_target = project.new_target( - :ui_test_bundle, 'RunnerUITests', :ios, deployment_target, nil, :objc -) - -group = project.main_group.find_subpath('RunnerUITests', true) -group.set_source_tree('SOURCE_ROOT') -file_ref = group.new_reference('RunnerUITests/RunnerUITests.m') -test_target.add_file_references([file_ref]) - -test_target.add_dependency(runner) - -test_target.build_configurations.each do |config| - s = config.build_settings - s['PRODUCT_BUNDLE_IDENTIFIER'] = 'io.getstream.flutter.RunnerUITests' - s['TEST_TARGET_NAME'] = 'Runner' - s['IPHONEOS_DEPLOYMENT_TARGET'] = deployment_target - s['CODE_SIGN_STYLE'] = 'Automatic' - s['CLANG_ENABLE_MODULES'] = 'YES' - s['SWIFT_VERSION'] = '5.0' - s['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = 'YES' - s['GENERATE_INFOPLIST_FILE'] = 'YES' - s['PRODUCT_NAME'] = '$(TARGET_NAME)' -end - -project.save - -scheme_path = File.join( - Xcodeproj::XCScheme.shared_data_dir(project_path), 'Runner.xcscheme' -) -scheme = Xcodeproj::XCScheme.new(scheme_path) -scheme.add_test_target(test_target) -scheme.save_as(project_path, 'Runner', true) - -puts "Added RunnerUITests target (bundle id io.getstream.flutter.RunnerUITests, "\ - "deployment #{deployment_target}) and wired it into the Runner scheme." From e97d34fa2cd65fc5f8d8c0379727b1aa54273209 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 18:07:56 +0100 Subject: [PATCH 47/66] Fix ios --- sample_app/fastlane/Fastfile | 3 +- .../ios/Runner.xcodeproj/project.pbxproj | 43 +++++++++++++++++++ .../xcshareddata/xcschemes/Runner.xcscheme | 5 ++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index e8e9efe3cc..6478b27bef 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -88,7 +88,6 @@ lane :run_e2e_test do |options| if ios device = prepare_simulator(device: options[:device], reset: true, wait_for_boot: false) ios_version = options[:ios] || ios_simulator_runtime_version(device) - build_e2e_test(platform: 'ios') unless options[:skip_build] end start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) @@ -130,7 +129,7 @@ lane :build_e2e_test do |options| end lane :build_and_run_e2e_test do |options| - build_e2e_test(platform: options[:platform]) unless options[:platform] == 'ios' + build_e2e_test(platform: options[:platform]) run_e2e_test(options) end diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 248d4b4798..ead95003f4 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 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 */; }; + A1B2C3D42D565B920041A301 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 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 */; }; @@ -100,6 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A1B2C3D42D565B920041A301 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, F06BB2D34B124C927609E541 /* Foundation.framework in Frameworks */, 984969FB0FCC70C82B1D01CC /* Pods_Runner_RunnerUITests.framework in Frameworks */, ); @@ -214,10 +216,12 @@ buildConfigurationList = 8A84C2230750F0B728CA3023 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; buildPhases = ( 3921E106080881E83E998BFB /* [CP] Check Pods Manifest.lock */, + E7F8A9B02D565B920041A30D /* xcode_backend build */, 08E8A4ECA2C1C2F244E229AF /* Sources */, C1289C2FFEC46190EEF8C3CE /* Frameworks */, 3DCFC2F9E1881E1CDF31C6A9 /* Resources */, BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */, + E7F8A9B12D565B940041A30D /* xcode_backend embed_and_thin */, ); buildRules = ( ); @@ -225,6 +229,9 @@ 7BBDFC82F80F89255BBCEC2F /* PBXTargetDependency */, ); name = RunnerUITests; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = RunnerUITests; productReference = 5F71825255CA4DD327F745DE /* RunnerUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -399,6 +406,42 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E7F8A9B02D565B920041A30D /* xcode_backend build */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "xcode_backend build"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + E7F8A9B12D565B940041A30D /* xcode_backend embed_and_thin */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "xcode_backend embed_and_thin"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d77590b84e..0b626a0958 100644 --- a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -3,7 +3,7 @@ LastUpgradeVersion = "1510" version = "1.7"> + skipped = "NO" + parallelizable = "NO"> Date: Mon, 13 Jul 2026 19:06:10 +0100 Subject: [PATCH 48/66] Get rid of Patrol --- .../actions/cache-ios-patrol-build/action.yml | 16 -- .github/workflows/e2e_test.yml | 92 +++---- .github/workflows/e2e_test_cron.yml | 20 +- .gitignore | 1 - melos.yaml | 5 +- sample_app/android/app/build.gradle | 2 - .../flutter/sample/MainActivityTest.java | 33 --- sample_app/fastlane/Allurefile | 1 + sample_app/fastlane/Fastfile | 45 ++-- .../integration_test/message_list_test.dart | 4 +- .../integration_test/robots/user_robot.dart | 23 +- .../user_robot_message_list_asserts.dart | 5 +- .../support/stream_test_case.dart | 10 +- .../support/stream_test_env.dart | 9 +- .../support/widget_test_extensions.dart | 55 ++++ sample_app/ios/Podfile | 4 - .../ios/Runner.xcodeproj/project.pbxproj | 255 ------------------ .../xcshareddata/xcschemes/Runner.xcscheme | 13 +- sample_app/ios/RunnerUITests/RunnerUITests.m | 5 - sample_app/pubspec.yaml | 11 - 20 files changed, 153 insertions(+), 456 deletions(-) delete mode 100644 .github/actions/cache-ios-patrol-build/action.yml delete mode 100644 sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java create mode 100644 sample_app/integration_test/support/widget_test_extensions.dart delete mode 100644 sample_app/ios/RunnerUITests/RunnerUITests.m diff --git a/.github/actions/cache-ios-patrol-build/action.yml b/.github/actions/cache-ios-patrol-build/action.yml deleted file mode 100644 index 72e891207f..0000000000 --- a/.github/actions/cache-ios-patrol-build/action.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: 'Cache iOS Patrol build' -description: 'Cache Patrol iOS integration test build artifacts' -inputs: - version: - description: 'Xcode / iOS simulator version used in the cache key' - required: true -runs: - using: "composite" - steps: - - uses: actions/cache@v6 - with: - path: sample_app/build/ios_integ - key: patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}-${{ hashFiles('**/pubspec.lock', 'sample_app/ios/Podfile', 'sample_app/pubspec.yaml') }} - restore-keys: | - patrol-ios-integ-${{ runner.os }}-xcode-${{ inputs.version }}- - patrol-ios-integ-${{ runner.os }}- diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index f99ffcc083..d4d5f78a66 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -21,55 +21,54 @@ env: ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }} jobs: - # android: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v6 + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 - # - uses: ./.github/actions/setup-java + - uses: ./.github/actions/setup-java - # - uses: ./.github/actions/cache-gradle + - uses: ./.github/actions/cache-gradle - # - uses: ./.github/actions/setup-ruby + - uses: ./.github/actions/setup-ruby - # - uses: ./.github/actions/setup-flutter + - uses: ./.github/actions/setup-flutter - # - name: Bootstrap - # run: | - # flutter pub global activate melos - # melos bootstrap - # dart pub global activate patrol_cli + - name: Bootstrap + run: | + flutter pub global activate melos + melos bootstrap - # - uses: ./.github/actions/enable-kvm + - uses: ./.github/actions/enable-kvm - # - name: Launch Allure TestOps - # run: cd sample_app/android && bundle exec fastlane allure_launch - # env: - # GITHUB_EVENT: ${{ toJson(github.event) }} + - name: Launch Allure TestOps + run: cd sample_app/android && bundle exec fastlane allure_launch + env: + GITHUB_EVENT: ${{ toJson(github.event) }} - # - name: Run e2e (Android emulator) - # uses: reactivecircus/android-emulator-runner@v2 - # with: - # api-level: ${{ env.android_api_level }} - # arch: x86_64 - # profile: pixel_6 - # script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 + - name: Run e2e (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.android_api_level }} + arch: x86_64 + profile: pixel_6 + script: cd sample_app/android && bundle exec fastlane run_e2e_test device:emulator-5554 - # - name: Upload Allure results - # if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) - # run: cd sample_app/android && bundle exec fastlane allure_upload + - name: Upload Allure results + if: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) + run: cd sample_app/android && bundle exec fastlane allure_upload - # - name: Allure TestOps Launch Removal - # if: env.ALLURE_LAUNCH_ID != '' && cancelled() - # run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID + - name: Allure TestOps Launch Removal + if: env.ALLURE_LAUNCH_ID != '' && cancelled() + run: cd sample_app/android && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - # - uses: actions/upload-artifact@v7 - # if: failure() - # with: - # name: e2e-android-logs - # path: | - # sample_app/stream-chat-test-mock-server/logs - # sample_app/build/app/reports + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: e2e-android-logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log ios: runs-on: macos-15 @@ -87,23 +86,18 @@ jobs: flutter precache --ios flutter pub global activate melos melos bootstrap - dart pub global activate patrol_cli - uses: ./.github/actions/cache-cocoapods with: key-suffix: ${{ runner.os }} - - uses: ./.github/actions/cache-ios-patrol-build - with: - version: ${{ env.ios_simulator_version }} - - name: Launch Allure TestOps run: cd sample_app/ios && bundle exec fastlane allure_launch env: GITHUB_EVENT: ${{ toJson(github.event) }} - name: Run e2e (iOS simulator) - run: cd sample_app/ios && bundle exec fastlane run_e2e_test platform:ios ios:"${{ env.ios_simulator_version }}" device:"${{ env.ios_simulator_device }}" verbose:true + 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: env.ALLURE_LAUNCH_ID != '' && (success() || failure()) @@ -113,14 +107,10 @@ jobs: if: env.ALLURE_LAUNCH_ID != '' && cancelled() run: cd sample_app/ios && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - - name: Parse xcresult - if: failure() - run: | - brew install chargepoint/xcparse/xcparse - xcparse logs sample_app/build/ios_integ/StreamChatUITestsApp.xcresult sample_app/build/ios_integ/logs/ - - uses: actions/upload-artifact@v7 if: always() with: name: e2e-ios-logs - path: sample_app/stream-chat-test-mock-server/logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log diff --git a/.github/workflows/e2e_test_cron.yml b/.github/workflows/e2e_test_cron.yml index fa584f75b8..95af832d03 100644 --- a/.github/workflows/e2e_test_cron.yml +++ b/.github/workflows/e2e_test_cron.yml @@ -44,7 +44,6 @@ jobs: run: | flutter pub global activate melos melos bootstrap - dart pub global activate patrol_cli - uses: ./.github/actions/enable-kvm @@ -73,7 +72,9 @@ jobs: if: failure() with: name: e2e-nightly-android-${{ matrix.api-level }} - path: sample_app/stream-chat-test-mock-server/logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log ios: runs-on: macos-15 @@ -105,7 +106,6 @@ jobs: 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 }} @@ -118,10 +118,6 @@ jobs: with: key-suffix: ${{ matrix.ios }} - - uses: ./.github/actions/cache-ios-patrol-build - with: - version: ${{ env.XCODE_VERSION }} - - name: Launch Allure TestOps run: cd sample_app/ios && bundle exec fastlane allure_launch cron:true @@ -136,17 +132,13 @@ jobs: if: env.ALLURE_LAUNCH_ID != '' && cancelled() run: cd sample_app/ios && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - - name: Parse xcresult - if: failure() - run: | - brew install chargepoint/xcparse/xcparse - xcparse logs sample_app/build/ios_integ/StreamChatUITestsApp.xcresult sample_app/build/ios_integ/logs/ - - uses: actions/upload-artifact@v7 if: failure() with: name: e2e-nightly-ios-${{ matrix.ios }} - path: sample_app/stream-chat-test-mock-server/logs + path: | + sample_app/stream-chat-test-mock-server/logs + sample_app/build/e2e-test.log slack: name: Slack Report diff --git a/.gitignore b/.gitignore index 39580332dc..69adba15b6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ *.log *.pyc /sample_app/stream-chat-test-mock-server/ -/sample_app/integration_test/test_bundle.dart /sample_app/allure-results/ /sample_app/allurectl *.swp diff --git a/melos.yaml b/melos.yaml index 4c93e7d398..3a112a2d69 100644 --- a/melos.yaml +++ b/melos.yaml @@ -126,7 +126,6 @@ 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 @@ -213,9 +212,9 @@ scripts: dirExists: test e2e:run: - run: melos exec -c 1 --depends-on="patrol" -- "patrol test" + run: melos exec -c 1 --scope="sample_app" -- "flutter test integration_test/" description: > - Run the Patrol e2e suite. Start the mock-server driver first + Run the integration_test e2e suite. Start the mock-server driver first (`fastlane start_mock_server`), or use the `run_e2e_test` Fastlane lane to orchestrate the mock server and tests in one step. diff --git a/sample_app/android/app/build.gradle b/sample_app/android/app/build.gradle index 8b7d252906..40bdd13b68 100644 --- a/sample_app/android/app/build.gradle +++ b/sample_app/android/app/build.gradle @@ -33,8 +33,6 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutter.versionCode versionName flutter.versionName - - 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 deleted file mode 100644 index 0c2861acf4..0000000000 --- a/sample_app/android/app/src/androidTest/java/io/getstream/chat/android/flutter/sample/MainActivityTest.java +++ /dev/null @@ -1,33 +0,0 @@ -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; - -@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/fastlane/Allurefile b/sample_app/fastlane/Allurefile index 90ad8a2f95..e97722c4ed 100644 --- a/sample_app/fastlane/Allurefile +++ b/sample_app/fastlane/Allurefile @@ -13,6 +13,7 @@ lane :collect_allure_results do |options| serial = options[:device] ? "-s #{options[:device]} " : '' `adb #{serial}logcat -d 2>/dev/null` end + log = [log, File.read(options[:log_file])].join("\n") if options[:log_file] && File.exist?(options[:log_file]) chunks = Hash.new { |hash, key| hash[key] = {} } log.scan(/ALLURE-RESULT::([0-9a-f-]+):(\d+):([A-Za-z0-9+\/=]+)/) do |uuid, seq, chunk| diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index 6478b27bef..cfbfd9c632 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -28,6 +28,13 @@ def ios_simulator_runtime_version(udid) nil end +lane :install_runtime do |options| + install_ios_runtime( + version: options[:ios], + custom_script: File.join(root_path, 'scripts/install_ios_runtime.sh'), + ) +end + lane :sh_on_root do |options| Dir.chdir(root_path) { sh(options[:command]) } end @@ -82,12 +89,10 @@ 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, wait_for_boot: false) - ios_version = options[:ios] || ios_simulator_runtime_version(device) end start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) @@ -96,6 +101,9 @@ lane :run_e2e_test do |options| sh("adb #{serial}logcat -c") end + log_file = "#{root_path}/build/e2e-test.log" + FileUtils.mkdir_p(File.dirname(log_file)) + begin Dir.chdir(root_path) do targets = options[:target] ? [options[:target]] : Dir['integration_test/*_test.dart'].sort @@ -103,43 +111,26 @@ lane :run_e2e_test do |options| failed = [] targets.each do |target| - flags = ["--target #{target}"] - flags << "--device #{device}" if device + flags = [target] + flags << "-d #{device}" if 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) + ok = sh_with_retries( + command: "flutter test #{flags.join(' ')} 2>&1 | tee -a #{log_file}", + retries: options[:retries], + label: target, + ) failed << target unless ok end UI.user_error!("E2E failed after retries: #{failed.join(', ')}") unless failed.empty? end ensure - collect_allure_results(device: device) rescue nil + collect_allure_results(device: device, log_file: log_file) rescue nil stop_mock_server end end -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("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 - -lane :install_runtime do |options| - install_ios_runtime( - version: options[:ios], - custom_script: File.join(root_path, 'scripts/install_ios_runtime.sh'), - ) -end - private_lane :sources_matrix do { e2e: [ diff --git a/sample_app/integration_test/message_list_test.dart b/sample_app/integration_test/message_list_test.dart index bc9a984dde..3bbe8b0331 100644 --- a/sample_app/integration_test/message_list_test.dart +++ b/sample_app/integration_test/message_list_test.dart @@ -11,9 +11,9 @@ void main() { streamTest( allureId: '11188', description: 'message list updates when the user sends a message', - body: ($) async { + body: (tester) async { final env = StreamTestEnv(); - await env.setUp($); + await env.setUp(tester); addTearDown(env.tearDown); await step('GIVEN the user opens a channel', () async { diff --git a/sample_app/integration_test/robots/user_robot.dart b/sample_app/integration_test/robots/user_robot.dart index 8fcc785fe1..9cfcb0c17a 100644 --- a/sample_app/integration_test/robots/user_robot.dart +++ b/sample_app/integration_test/robots/user_robot.dart @@ -1,33 +1,34 @@ -import 'package:patrol/patrol.dart'; +import 'package:flutter_test/flutter_test.dart'; import '../pages/channel_list_page.dart'; import '../pages/message_list_page.dart'; import '../support/predefined_users.dart'; +import '../support/widget_test_extensions.dart'; class UserRobot { - UserRobot(this.$); + UserRobot(this.tester); - final PatrolIntegrationTester $; + final WidgetTester tester; Future login([ UserCredentials user = PredefinedUsers.currentUser, ]) async { - final entry = $(user.name); - await entry.scrollTo(); - await entry.tap(); + await tester.scrollToText(user.name); + await tester.tapText(user.name); return this; } Future openChannel({int index = 0}) async { - final tile = $(ChannelListPage.channelTile).at(index); - await tile.waitUntilVisible(); - await tile.tap(); + final tile = find.byType(ChannelListPage.channelTile).at(index); + await tester.waitUntilVisible(tile); + await tester.tap(tile); + await tester.pumpAndSettle(); return this; } Future sendMessage(String text) async { - await $(MessageListPage.composer.inputField).enterText(text); - await $(MessageListPage.composer.sendButton).tap(); + await tester.enterTextInField(MessageListPage.composer.inputField, text); + await tester.tapByKey(MessageListPage.composer.sendButton); return this; } } diff --git a/sample_app/integration_test/robots/user_robot_message_list_asserts.dart b/sample_app/integration_test/robots/user_robot_message_list_asserts.dart index 0b0fa8fdef..fb1fe58cf2 100644 --- a/sample_app/integration_test/robots/user_robot_message_list_asserts.dart +++ b/sample_app/integration_test/robots/user_robot_message_list_asserts.dart @@ -1,8 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; + +import '../support/widget_test_extensions.dart'; import 'user_robot.dart'; extension UserRobotMessageListAsserts on UserRobot { Future assertMessage(String text) async { - await $(text).waitUntilVisible(); + await tester.waitUntilVisible(find.text(text)); return this; } } diff --git a/sample_app/integration_test/support/stream_test_case.dart b/sample_app/integration_test/support/stream_test_case.dart index 80227ed3a3..70f05677ce 100644 --- a/sample_app/integration_test/support/stream_test_case.dart +++ b/sample_app/integration_test/support/stream_test_case.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:patrol/patrol.dart'; +import 'package:integration_test/integration_test.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart' show Invoker; @@ -9,16 +9,18 @@ import '../allure/allure.dart'; void streamTest({ String? allureId, required String description, - required Future Function(PatrolIntegrationTester $) body, + required Future Function(WidgetTester tester) body, }) { - patrolTest(description, ($) async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets(description, (tester) async { Allure.instance.startTest( name: description, fullName: Invoker.current?.liveTest.test.name ?? description, labels: {if (allureId != null) 'AS_ID': allureId}, ); try { - await body($); + await body(tester); Allure.instance.stopTest(status: AllureStatus.passed); } on TestFailure catch (e, st) { Allure.instance.stopTest(status: AllureStatus.failed, message: e, trace: st); diff --git a/sample_app/integration_test/support/stream_test_env.dart b/sample_app/integration_test/support/stream_test_env.dart index 643718900e..dc8ca9b05d 100644 --- a/sample_app/integration_test/support/stream_test_env.dart +++ b/sample_app/integration_test/support/stream_test_env.dart @@ -1,4 +1,4 @@ -import 'package:patrol/patrol.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:sample_app/app.dart'; import 'package:sample_app/auth/auth_controller.dart'; @@ -13,18 +13,19 @@ class StreamTestEnv { late final ParticipantRobot participantRobot; late final UserRobot userRobot; - Future setUp(PatrolIntegrationTester $) async { + Future setUp(WidgetTester tester) async { mockServer = await MockServer.start(); backendRobot = BackendRobot(mockServer); participantRobot = ParticipantRobot(mockServer); - userRobot = UserRobot($); + userRobot = UserRobot(tester); authController.debugConnectionOverride = StreamConnectionOverride( baseURL: mockServer.url, baseWsUrl: mockServer.wsUrl, ); - await $.pumpWidgetAndSettle(const StreamChatSampleApp()); + await tester.pumpWidget(const StreamChatSampleApp()); + await tester.pumpAndSettle(); } Future tearDown() async { diff --git a/sample_app/integration_test/support/widget_test_extensions.dart b/sample_app/integration_test/support/widget_test_extensions.dart new file mode 100644 index 0000000000..d7a518ec14 --- /dev/null +++ b/sample_app/integration_test/support/widget_test_extensions.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension E2EWidgetTester on WidgetTester { + Future scrollToText(String text) async { + await scrollUntilVisible( + find.text(text), + 500, + scrollable: find.byType(Scrollable).first, + ); + await pumpAndSettle(); + } + + Future tapText(String text) async { + await waitUntilVisible(find.text(text)); + await tap(find.text(text)); + await pumpAndSettle(); + } + + Future tapByType(Type type, {int index = 0}) async { + final finder = find.byType(type); + await waitUntilVisible(finder.at(index)); + await tap(finder.at(index)); + await pumpAndSettle(); + } + + Future tapByKey(Key key) async { + final finder = find.byKey(key); + await waitUntilVisible(finder); + await tap(finder); + await pumpAndSettle(); + } + + Future enterTextInField(Type inputFieldType, String text) async { + final finder = find.byType(inputFieldType); + await waitUntilVisible(finder); + await enterText(finder, text); + await pumpAndSettle(); + } + + Future waitUntilVisible( + Finder finder, { + Duration timeout = const Duration(seconds: 30), + }) async { + final end = DateTime.now().add(timeout); + while (DateTime.now().isBefore(end)) { + await pump(const Duration(milliseconds: 100)); + if (finder.evaluate().isNotEmpty) { + await pumpAndSettle(); + return; + } + } + throw TestFailure('Timed out waiting for $finder'); + } +} diff --git a/sample_app/ios/Podfile b/sample_app/ios/Podfile index 07efaa87f8..f17bddc919 100644 --- a/sample_app/ios/Podfile +++ b/sample_app/ios/Podfile @@ -31,10 +31,6 @@ target 'Runner' do use_frameworks! use_modular_headers! - 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 ead95003f4..2efc89d841 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -16,23 +16,9 @@ 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 */; }; - A1B2C3D42D565B920041A301 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 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; @@ -60,19 +46,14 @@ 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 = ""; }; @@ -84,7 +65,6 @@ 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 */ @@ -97,27 +77,9 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C1289C2FFEC46190EEF8C3CE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A1B2C3D42D565B920041A301 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 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 = ( @@ -131,7 +93,6 @@ children = ( 4899F8CDC11D7148C6179369 /* Pods_Runner.framework */, 848E793A104447E77BE01B37 /* iOS */, - 2979FD476C8342D4BE16EDE3 /* Pods_Runner_RunnerUITests.framework */, ); name = Frameworks; sourceTree = ""; @@ -157,7 +118,6 @@ CF168B61BAB91958681C7C21 /* Pods */, DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */, 9278E7173A28D18D316BCCAC /* Frameworks */, - 7926AAA8A8EB00DB5D149360 /* RunnerUITests */, ); sourceTree = ""; }; @@ -165,7 +125,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 5F71825255CA4DD327F745DE /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -201,9 +160,6 @@ 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 = ""; @@ -211,31 +167,6 @@ /* 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 */, - E7F8A9B02D565B920041A30D /* xcode_backend build */, - 08E8A4ECA2C1C2F244E229AF /* Sources */, - C1289C2FFEC46190EEF8C3CE /* Frameworks */, - 3DCFC2F9E1881E1CDF31C6A9 /* Resources */, - BE63BFB6842CF1965047723E /* [CP] Embed Pods Frameworks */, - E7F8A9B12D565B940041A30D /* xcode_backend embed_and_thin */, - ); - buildRules = ( - ); - dependencies = ( - 7BBDFC82F80F89255BBCEC2F /* PBXTargetDependency */, - ); - name = RunnerUITests; - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); - 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" */; @@ -295,19 +226,11 @@ 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; @@ -323,28 +246,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3921E106080881E83E998BFB /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-RunnerUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - 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; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -368,23 +269,19 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-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; @@ -406,72 +303,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - E7F8A9B02D565B920041A30D /* xcode_backend build */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "xcode_backend build"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; - E7F8A9B12D565B940041A30D /* xcode_backend embed_and_thin */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "xcode_backend embed_and_thin"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; - }; - 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; @@ -497,14 +328,6 @@ /* 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; @@ -517,15 +340,6 @@ }; /* 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; @@ -546,26 +360,6 @@ /* 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - 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 = { @@ -650,45 +444,6 @@ }; 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - 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 = { @@ -865,16 +620,6 @@ /* 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 = ( diff --git a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0b626a0958..16b76ec3c8 100644 --- a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -3,7 +3,7 @@ LastUpgradeVersion = "1510" version = "1.7"> - - - - =2.2.5 <2.3.0" rxdart: ^0.28.0 stream_chat_flutter: ^10.1.0 @@ -55,18 +53,9 @@ 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: - app_name: Stream Chat - test_directory: integration_test - android: - package_name: io.getstream.chat.android.flutter.sample - ios: - bundle_id: io.getstream.flutter From 91780cd65b4cf118c62794a06f7ebb3d856b0af6 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Jul 2026 19:26:26 +0100 Subject: [PATCH 49/66] Beautify logs --- .github/workflows/e2e_test.yml | 2 +- sample_app/fastlane/Fastfile | 18 +++++- sample_app/scripts/pretty_e2e_log.rb | 94 ++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100755 sample_app/scripts/pretty_e2e_log.rb diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index d4d5f78a66..4fa817d728 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -108,7 +108,7 @@ jobs: run: cd sample_app/ios && bundle exec fastlane allure_launch_removal launch_id:$ALLURE_LAUNCH_ID - uses: actions/upload-artifact@v7 - if: always() + if: failure() with: name: e2e-ios-logs path: | diff --git a/sample_app/fastlane/Fastfile b/sample_app/fastlane/Fastfile index cfbfd9c632..4801d4a577 100644 --- a/sample_app/fastlane/Fastfile +++ b/sample_app/fastlane/Fastfile @@ -102,6 +102,7 @@ lane :run_e2e_test do |options| end log_file = "#{root_path}/build/e2e-test.log" + pretty_log = File.join(root_path, 'scripts/pretty_e2e_log.rb') FileUtils.mkdir_p(File.dirname(log_file)) begin @@ -109,14 +110,27 @@ lane :run_e2e_test do |options| targets = options[:target] ? [options[:target]] : Dir['integration_test/*_test.dart'].sort UI.user_error!('No e2e test files found') if targets.empty? + reporter = ENV['GITHUB_ACTIONS'] ? 'github' : 'expanded' failed = [] targets.each do |target| flags = [target] flags << "-d #{device}" if device - flags << '--verbose' if options[:verbose] + if options[:verbose] + flags << '--verbose' + else + flags << '--quiet' + flags << "--reporter #{reporter}" + end + + flutter_cmd = "flutter test #{flags.join(' ')}" + command = if options[:verbose] + "#{flutter_cmd} 2>&1 | tee -a #{log_file}" + else + "set -o pipefail; #{flutter_cmd} 2>&1 | tee -a #{log_file} | ruby #{pretty_log}" + end ok = sh_with_retries( - command: "flutter test #{flags.join(' ')} 2>&1 | tee -a #{log_file}", + command: command, retries: options[:retries], label: target, ) diff --git a/sample_app/scripts/pretty_e2e_log.rb b/sample_app/scripts/pretty_e2e_log.rb new file mode 100755 index 0000000000..4a6f3010bd --- /dev/null +++ b/sample_app/scripts/pretty_e2e_log.rb @@ -0,0 +1,94 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Filters noisy flutter integration_test device-build output for the console. +# Full unfiltered output should be tee'd to a log file before this script. + +module PrettyE2ELog + module_function + + NOISE = / + \A\s*(?:CompileC|CompileSwift|Ld |ProcessInfoPlistFile|Copy (?:Swift|\/|headers))| + \A\s*(?:note:|warning: \[) | + \A\s*export (?:ARCHS|BUILD_DIR|CONFIGURATION|DEVELOPMENT_TEAM)=| + \A\s*\*\* BUILD | + \A\s*>\s*Task | + \A\s*Transforming | + \A(?:In file included from|\ \ \[|\ \ \])| + \A\s*(?:WriteAuxiliaryFile|RegisterExecutionPolicyException|Touch |CodeSign | + ValidateEmbeddedBinary|PhaseScriptExecution|ExtractAppIntentsMetadata| + GenerateDSYMFile|SwiftEmitModule|SwiftDriver|SwiftCompile|ScanDependencies| + CpResource|CpHeader|ProcessProductPackaging|Libtool|GenerateTAPI| + AppIntentsMetadata|CreateUniversalBinary|LinkStoryboards|CompileAssetCatalog)| + \A\s*ALLURE-RESULT:: + /x + + TEST_LINE = /\A(?