diff --git a/.cirrus.yml b/.cirrus.yml index d71032c87..4a8c5b175 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,7 +11,7 @@ test_linux_task: kvm: 'true' env: PATH: $HOME/.pub-cache/bin:$HOME/fvm/default/bin:$CIRRUS_WORKING_DIR/fvm:${PATH} - FLUTTER_VERSION: '3.13' + FLUTTER_VERSION: '3.16.0-0.5.pre' EMULATOR_API_LEVEL: '34' EMULATOR_ABI: google_apis_playstore;x86_64 EMULATOR_IMAGE: system-images;android-${EMULATOR_API_LEVEL};${EMULATOR_ABI} @@ -20,26 +20,15 @@ test_linux_task: set_up_fvm_script: | curl -LO https://github.com/fluttertools/fvm/releases/download/2.4.1/fvm-2.4.1-linux-x64.tar.gz tar -xf fvm-2.4.1-linux-x64.tar.gz - find_latest_matching_flutter_version_script: | - flutter_versions=$(fvm releases) - flutter_matching_versions=$(echo "$flutter_versions" | grep -v "pre" | grep -o " ${FLUTTER_VERSION}.[0-9]*") - latest_flutter_matching_version=$(echo "$flutter_matching_versions" | sort -rV | head -n 1) - echo "LATEST_MATCHING_FLUTTER_VERSION=$latest_flutter_matching_version" >> $CIRRUS_ENV setup_flutter_script: | - echo "y" | fvm global $LATEST_MATCHING_FLUTTER_VERSION + echo "y" | fvm global $FLUTTER_VERSION fvm doctor flutter --version - flutter precache - generate_gradlew_script: | - cd packages/patrol/example - flutter build apk --target lib/main.dart --debug --flavor=does-not-exist & - start=$SECONDS - until [ -e "android/gradlew" ] || [ $(($SECONDS - start)) -ge 120 ]; do sleep 2; done - if [ ! -e "android/gradlew" ]; then - echo "android/gradlew was not generated within the 2 minutes timeout" - exit 1 - fi - kill $! + flutter precache --android + cd packages/patrol/example && flutter build apk --config-only + melos_bootstrap_script: | + dart pub global activate melos + melos bootstrap setup_patrol_cli_script: - dart pub global activate --source path packages/patrol_cli && patrol setup_emulator_script: | @@ -74,25 +63,23 @@ test_macos_task: - 'package: patrol' - 'cirrusci' macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:latest + image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest env: PATH: $HOME/.pub-cache/bin:$HOME/fvm/default/bin:${PATH} - FLUTTER_VERSION: '3.13' + FLUTTER_VERSION: '3.16.0-0.5.pre' timeout_in: 30m set_up_fvm_script: | brew tap leoafarias/fvm brew install fvm - find_latest_matching_flutter_version_script: | - flutter_versions=$(fvm releases) - flutter_matching_versions=$(echo "$flutter_versions" | grep -v "pre" | grep -o " ${FLUTTER_VERSION}.[0-9]*") - latest_flutter_matching_version=$(echo "$flutter_matching_versions" | sort -rV | head -n 1) - echo "LATEST_MATCHING_FLUTTER_VERSION=$latest_flutter_matching_version" >> $CIRRUS_ENV setup_flutter_script: | - echo "y" | fvm global $LATEST_MATCHING_FLUTTER_VERSION + echo "y" | fvm global $FLUTTER_VERSION fvm doctor flutter --version - flutter precache + flutter precache --ios + melos_bootstrap_script: | + dart pub global activate melos + melos bootstrap setup_patrol_cli_script: - dart pub global activate --source path packages/patrol_cli && patrol setup_simulator_script: | diff --git a/.github/workflows/adb-prepare.yaml b/.github/workflows/adb-prepare.yaml index 7dc07b27c..83563f887 100644 --- a/.github/workflows/adb-prepare.yaml +++ b/.github/workflows/adb-prepare.yaml @@ -8,13 +8,13 @@ on: jobs: main: - name: Dart ${{ matrix.sdk }} + name: Dart ${{ matrix.dart-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - sdk: ['2.18.0', stable] + dart-version: ['3.1'] defaults: run: @@ -22,7 +22,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Dart uses: dart-lang/setup-dart@v1 diff --git a/.github/workflows/adb-publish.yaml b/.github/workflows/adb-publish.yaml index 06b58b8cf..3adc1cc39 100644 --- a/.github/workflows/adb-publish.yaml +++ b/.github/workflows/adb-publish.yaml @@ -15,7 +15,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Dart uses: dart-lang/setup-dart@v1 diff --git a/.github/workflows/label_pull_request.yaml b/.github/workflows/label_pull_request.yaml index 3004f27f9..396dbc7c8 100644 --- a/.github/workflows/label_pull_request.yaml +++ b/.github/workflows/label_pull_request.yaml @@ -9,9 +9,10 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Label Pull Request uses: actions/labeler@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + sync-labels: '' diff --git a/.github/workflows/patrol-prepare.yaml b/.github/workflows/patrol-prepare.yaml index 6b669996d..b6cc35e97 100644 --- a/.github/workflows/patrol-prepare.yaml +++ b/.github/workflows/patrol-prepare.yaml @@ -15,6 +15,8 @@ jobs: fail-fast: false matrix: os: [windows-latest] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -22,12 +24,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.13.x' + uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v3 @@ -35,9 +32,18 @@ jobs: distribution: temurin java-version: 17 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} + + - name: Preload Flutter artifacts + run: flutter precache --android + - name: Generate Gradle wrapper working-directory: packages/patrol/example - run: flutter build apk --debug --flavor=does-not-exist || true + run: flutter build apk --config-only - name: ktlint check run: .\gradlew.bat :patrol:ktlintCheck @@ -59,6 +65,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -66,16 +74,26 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.13.x' + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} + + - name: Preload Flutter artifacts + run: flutter precache --android - name: Generate Gradle wrapper working-directory: packages/patrol/example - run: flutter build apk --debug --flavor=does-not-exist || true + run: flutter build apk --config-only - name: Run unit tests if: success() || failure() @@ -103,6 +121,8 @@ jobs: fail-fast: false matrix: os: [macos-latest] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -110,7 +130,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install tools run: | @@ -136,7 +156,11 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.13.x' + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} + + - name: Preload Flutter artifacts + run: flutter precache --ios - name: Generate iOS build files working-directory: packages/patrol/example @@ -170,7 +194,8 @@ jobs: strategy: fail-fast: false matrix: - flutter-version: ['3.3.x', '3.7.x', '3.10.x', '3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -178,12 +203,19 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} + + - name: Set up Melos and activate workspace + working-directory: . + run: | + dart pub global activate melos + melos bootstrap - name: flutter pub get run: flutter pub get @@ -197,7 +229,7 @@ jobs: run: flutter analyze - name: dart format - if: ${{ (success() || failure()) && (contains('3.10.x', matrix.flutter-version) || contains('3.13.x', matrix.flutter-version)) }} + if: success() || failure() run: dart format --set-exit-if-changed . - name: flutter pub publish --dry-run diff --git a/.github/workflows/patrol-publish.yaml b/.github/workflows/patrol-publish.yaml index ba99b9de6..354632820 100644 --- a/.github/workflows/patrol-publish.yaml +++ b/.github/workflows/patrol-publish.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # This step adds the auth token for pub.dev - name: Set up Dart diff --git a/.github/workflows/patrol_cli-prepare.yaml b/.github/workflows/patrol_cli-prepare.yaml index e5af71889..883ef2482 100644 --- a/.github/workflows/patrol_cli-prepare.yaml +++ b/.github/workflows/patrol_cli-prepare.yaml @@ -15,7 +15,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - flutter-version: ['3.3.x', '3.7.x', '3.10.x', '3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -23,12 +24,13 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Run flutter tool (to dismiss the first run experience) run: flutter diff --git a/.github/workflows/patrol_cli-publish.yaml b/.github/workflows/patrol_cli-publish.yaml index a6b6315fa..ec4c05619 100644 --- a/.github/workflows/patrol_cli-publish.yaml +++ b/.github/workflows/patrol_cli-publish.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check if versions are defined consistently working-directory: packages/patrol_cli diff --git a/.github/workflows/patrol_devtools_extension-prepare.yaml b/.github/workflows/patrol_devtools_extension-prepare.yaml new file mode 100644 index 000000000..b20eb6089 --- /dev/null +++ b/.github/workflows/patrol_devtools_extension-prepare.yaml @@ -0,0 +1,51 @@ +name: patrol_devtools_extension prepare + +on: + workflow_dispatch: + pull_request: + paths: + - 'packages/patrol_devtools_extension/**' + +jobs: + prepare: + name: Flutter ${{ matrix.flutter-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] + + defaults: + run: + working-directory: packages/patrol_devtools_extension + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} + + - name: flutter pub get + run: flutter pub get + + - name: flutter test + if: success() || failure() + run: flutter test --coverage + + - name: flutter analyze + if: success() || failure() + run: flutter analyze + + - name: dart format + if: success() || failure() + run: dart format --set-exit-if-changed . + + - name: Build extension + if: success() || failure() + run: ./publish_to_patrol_extension diff --git a/.github/workflows/patrol_finders-prepare.yaml b/.github/workflows/patrol_finders-prepare.yaml index b9513f64b..94c0594e0 100644 --- a/.github/workflows/patrol_finders-prepare.yaml +++ b/.github/workflows/patrol_finders-prepare.yaml @@ -14,7 +14,8 @@ jobs: strategy: fail-fast: false matrix: - flutter-version: ['3.3.x', '3.7.x', '3.10.x', '3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -22,12 +23,13 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: flutter pub get run: flutter pub get diff --git a/.github/workflows/patrol_finders-publish.yaml b/.github/workflows/patrol_finders-publish.yaml index bef605e81..8f86bd593 100644 --- a/.github/workflows/patrol_finders-publish.yaml +++ b/.github/workflows/patrol_finders-publish.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # This step adds the auth token for pub.dev - name: Set up Dart diff --git a/.github/workflows/patrol_gen-prepare.yaml b/.github/workflows/patrol_gen-prepare.yaml index f8a7ab0d3..c5e20df2a 100644 --- a/.github/workflows/patrol_gen-prepare.yaml +++ b/.github/workflows/patrol_gen-prepare.yaml @@ -8,13 +8,13 @@ on: jobs: prepare: - name: Flutter ${{ matrix.flutter-version }} + name: Dart ${{ matrix.dart-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - sdk: ['3.0.0', stable] + dart-version: ['3.1'] defaults: run: @@ -22,12 +22,12 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Dart uses: dart-lang/setup-dart@v1 with: - sdk: ${{ matrix.sdk }} + sdk: ${{ matrix.dart-version }} - name: dart pub get run: dart pub get diff --git a/.github/workflows/test-android-device.yaml b/.github/workflows/test-android-device.yaml index e53ea4a75..54c08d2d9 100644 --- a/.github/workflows/test-android-device.yaml +++ b/.github/workflows/test-android-device.yaml @@ -7,11 +7,11 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} ${{ matrix.os_version }} + name: Flutter ${{ matrix.flutter-version }} on ${{ matrix.os }} ${{ matrix.os_version }} runs-on: ubuntu-latest timeout-minutes: 30 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} ${{ matrix.os_version }} + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} on ${{ matrix.os }} ${{ matrix.os_version }} TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} EXCLUDED_TESTS: ${{ steps.set_excluded_tests.outputs.EXCLUDED_TESTS }} URL_TO_DETAILS: ${{ steps.tests_step.outputs.URL_TO_DETAILS }} @@ -19,7 +19,8 @@ jobs: strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] device_model: ['oriole'] os: ['Android API'] os_version: ['33'] @@ -30,7 +31,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v3 @@ -54,17 +55,24 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --android + + - name: Set up Melos and activate workspace + working-directory: . + run: | + dart pub global activate melos + melos bootstrap - name: Set up Patrol CLI working-directory: packages/patrol_cli run: dart pub global activate --source path . && patrol - name: Generate Gradle wrapper - run: flutter build apk --debug --flavor=does-not-exist || true + run: flutter build apk --config-only - name: Set tests to exclude id: set_excluded_tests diff --git a/.github/workflows/test-android-emulator-webview.yaml b/.github/workflows/test-android-emulator-webview.yaml index f3e06d921..1d8f0c8f3 100644 --- a/.github/workflows/test-android-emulator-webview.yaml +++ b/.github/workflows/test-android-emulator-webview.yaml @@ -7,18 +7,19 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} webview on emulator.wtf + name: Flutter ${{ matrix.flutter-version }} webview on emulator.wtf runs-on: ubuntu-latest timeout-minutes: 30 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} webview on emulator.wtf + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} webview on emulator.wtf TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} URL_TO_DETAILS: ${{ steps.tests_step.outputs.URL_TO_DETAILS }} strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -26,7 +27,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v3 @@ -42,17 +43,18 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --android - name: Set up Patrol CLI working-directory: packages/patrol_cli run: dart pub global activate --source path . && patrol - name: Generate Gradle wrapper - run: flutter build apk --debug --flavor=does-not-exist || true + run: flutter build apk --config-only - name: Install ew-cli run: | diff --git a/.github/workflows/test-android-emulator.yaml b/.github/workflows/test-android-emulator.yaml index d40fa265b..486ad5aa7 100644 --- a/.github/workflows/test-android-emulator.yaml +++ b/.github/workflows/test-android-emulator.yaml @@ -7,11 +7,11 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} on emulator.wtf + name: Flutter ${{ matrix.flutter-version }} on emulator.wtf runs-on: ubuntu-latest timeout-minutes: 30 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} on emulator.wtf + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} on emulator.wtf TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} EXCLUDED_TESTS: ${{ steps.set_excluded_tests.outputs.EXCLUDED_TESTS }} URL_TO_DETAILS: ${{ steps.tests_step.outputs.URL_TO_DETAILS }} @@ -19,7 +19,8 @@ jobs: strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] defaults: run: @@ -27,7 +28,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v3 @@ -43,17 +44,24 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --android + + - name: Set up Melos and activate workspace + working-directory: . + run: | + dart pub global activate melos + melos bootstrap - name: Set up Patrol CLI working-directory: packages/patrol_cli run: dart pub global activate --source path . && patrol - name: Generate Gradle wrapper - run: flutter build apk --debug --flavor=does-not-exist || true + run: flutter build apk --config-only - name: Install ew-cli run: | diff --git a/.github/workflows/test-ios-device.yaml b/.github/workflows/test-ios-device.yaml index 09dcff662..402ce9630 100644 --- a/.github/workflows/test-ios-device.yaml +++ b/.github/workflows/test-ios-device.yaml @@ -7,11 +7,11 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} on ${{ matrix.device_model }} ${{ matrix.os }} ${{ matrix.os_version }} on FTL + name: Flutter ${{ matrix.flutter-version }} on ${{ matrix.device_model }} ${{ matrix.os }} ${{ matrix.os_version }} on FTL runs-on: macos-latest timeout-minutes: 40 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} ${{ matrix.os_version }} + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} on ${{ matrix.os }} ${{ matrix.os_version }} TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} EXCLUDED_TESTS: ${{ steps.set_excluded_tests.outputs.EXCLUDED_TESTS }} URL_TO_DETAILS: ${{ steps.tests_step.outputs.URL_TO_DETAILS }} @@ -19,7 +19,8 @@ jobs: strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] device_model: ['iphone14pro'] os_version: ['16.6'] os: [iOS] @@ -30,7 +31,7 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Add current platform to Gemfile working-directory: packages/patrol/example/ios @@ -69,10 +70,17 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --ios + + - name: Set up Melos and activate workspace + working-directory: . + run: | + dart pub global activate melos + melos bootstrap - name: Set up Patrol CLI working-directory: packages/patrol_cli diff --git a/.github/workflows/test-ios-simulator-webview.yaml b/.github/workflows/test-ios-simulator-webview.yaml index dbbf59bb1..a3ba0f62a 100644 --- a/.github/workflows/test-ios-simulator-webview.yaml +++ b/.github/workflows/test-ios-simulator-webview.yaml @@ -7,11 +7,11 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} webview on ${{ matrix.device_model }} (${{ matrix.os_version }}) simulator + name: Flutter ${{ matrix.flutter-version }} webview on ${{ matrix.device_model }} (${{ matrix.os_version }}) simulator runs-on: macos-latest timeout-minutes: 40 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} ${{ matrix.os_version }} simulator + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} on ${{ matrix.os }} ${{ matrix.os_version }} simulator TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} FAILURE_STATUS: ${{ steps.status_step.outputs.FAILURE_STATUS }} ERROR_STATUS: ${{ steps.status_step.outputs.ERROR_STATUS }} @@ -19,7 +19,8 @@ jobs: strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] device_model: [iPhone 14] os: [iOS] os_version: ['16.2'] @@ -30,15 +31,16 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --ios - name: Set up Patrol CLI working-directory: packages/patrol_cli diff --git a/.github/workflows/test-ios-simulator.yaml b/.github/workflows/test-ios-simulator.yaml index 556affeb0..7fc2184c0 100644 --- a/.github/workflows/test-ios-simulator.yaml +++ b/.github/workflows/test-ios-simulator.yaml @@ -7,11 +7,11 @@ on: jobs: run_tests: - name: Flutter ${{ matrix.flutter_version }} on ${{ matrix.device_model }} (${{ matrix.os_version }}) simulator + name: Flutter ${{ matrix.flutter-version }} on ${{ matrix.device_model }} (${{ matrix.os_version }}) simulator runs-on: macos-latest timeout-minutes: 40 outputs: - SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter_version }} on ${{ matrix.os }} ${{ matrix.os_version }} simulator + SLACK_MESSAGE_TITLE: Flutter ${{ matrix.flutter-version }} on ${{ matrix.os }} ${{ matrix.os_version }} simulator TESTS_EXIT_CODE: ${{ steps.tests_step.outputs.TESTS_EXIT_CODE }} FAILURE_STATUS: ${{ steps.status_step.outputs.FAILURE_STATUS }} ERROR_STATUS: ${{ steps.status_step.outputs.ERROR_STATUS }} @@ -20,7 +20,8 @@ jobs: strategy: fail-fast: false matrix: - flutter_version: ['3.13.x'] + flutter-version: ['3.16.0-0.5.pre'] + flutter-channel: ['beta'] device_model: [iPhone 8, iPhone 14, iPad (9th generation)] os: [iOS] os_version: ['16.2'] @@ -31,15 +32,22 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: ${{ matrix.flutter_version }} + flutter-version: ${{ matrix.flutter-version }} + channel: ${{ matrix.flutter-channel }} - name: Preload Flutter artifacts - run: flutter precache + run: flutter precache --ios + + - name: Set up Melos and activate workspace + working-directory: . + run: | + dart pub global activate melos + melos bootstrap - name: Set up Patrol CLI working-directory: packages/patrol_cli diff --git a/dev/cli_tests/patrol_develop_test.dart b/dev/cli_tests/patrol_develop_test.dart index 45feec394..c61102c66 100644 --- a/dev/cli_tests/patrol_develop_test.dart +++ b/dev/cli_tests/patrol_develop_test.dart @@ -38,7 +38,7 @@ void main() { void main(List args) async { _verifyWorkingDirectory(); - const afterBuildCompletedTimeout = Duration(minutes: 2); + const afterBuildCompletedTimeout = Duration(minutes: 4); const inactivityTimeout = Duration(minutes: 15); var isFirstTestPassed = false; @@ -58,6 +58,7 @@ void main(List args) async { [ 'develop', ...['--target', 'integration_test/example_test.dart'], + ...['--no-open-devtools'], ...args, ], runInShell: true, @@ -109,7 +110,9 @@ void main(List args) async { if (stringOutput.contains('Completed building')) { inactivityTimer = Timer(afterBuildCompletedTimeout, () { - print('Two minutes of inactivity, something went wrong...'); + print( + '${afterBuildCompletedTimeout.inSeconds} seconds of inactivity, something went wrong...', + ); print('isFirstTestPassed: $isFirstTestPassed'); print('isReloaded: $isReloaded'); print('Running file:'); diff --git a/dev/cli_tests/pubspec.yaml b/dev/cli_tests/pubspec.yaml index df8219e5d..bf7d14aee 100644 --- a/dev/cli_tests/pubspec.yaml +++ b/dev/cli_tests/pubspec.yaml @@ -2,10 +2,10 @@ name: cli_tests description: Scripts to test patrol_cli. environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dependencies: - path: ^1.8.0 + path: ^1.8.3 dev_dependencies: - leancode_lint: ^3.0.0 + leancode_lint: ^7.0.0+1 diff --git a/packages/adb/pubspec.yaml b/packages/adb/pubspec.yaml index c74cf188c..362dda167 100644 --- a/packages/adb/pubspec.yaml +++ b/packages/adb/pubspec.yaml @@ -5,14 +5,14 @@ repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.1.0 <4.0.0' dependencies: mocktail: ^0.3.0 path: ^1.8.3 dev_dependencies: - leancode_lint: ^3.0.0 + leancode_lint: ^7.0.0+1 test: ^1.22.2 executables: diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 08f152b98..a0ef2a75f 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,28 @@ +## Unreleased + +Give a warm welcome to the new **Patrol DevTools Extension**! + +Patrol DevTools extension allows you to explore the native view hierarchy when +developing tests with `patrol develop`. . Now you can easily see what Android / +iOS views are currently visible and discover their properties so that they can +be used in native selectors like `$.native.tap()`. You don’t have to use any +external tools for that. This is just the beginning, and we plan to add more +features to our extension in the future. + +Other changes: + +- Bump minimum supported Flutter version to 3.16 +- **BREAKING:** + - Remove `bindingType` parameter from `patrolTest()` function. Now only + `PatrolBinding` is used and it's automatically initialized (#1882) + - Remove `nativeAutomation` parameter from `patrolTest()` function. Now it's + enabled by default (#1882) + - This release also depends on [patrol_finders + v2](https://pub.dev/packages/patrol_finders/changelog#200) and includes + its breaking changes. + +- Remove dependency on `integration_test` plugin (#1882) + ## 2.3.2 - Add `PatrolFinder.longPress()` (#1825) diff --git a/packages/patrol/analysis_options.yaml b/packages/patrol/analysis_options.yaml index 76a7fea8a..36c9c08a3 100644 --- a/packages/patrol/analysis_options.yaml +++ b/packages/patrol/analysis_options.yaml @@ -1,12 +1,5 @@ include: package:leancode_lint/analysis_options_package.yaml analyzer: - errors: - deprecated_member_use: ignore # TODO: Remove after deprections are fixed exclude: - - lib/**/*.pb.dart - - lib/**/*.pbenum.dart - - lib/**/*.pbjson.dart - - lib/**/*.pbserver.dart - - lib/**/*.pbgrpc.dart - lib/**/*.g.dart diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index a1098bf6d..544602259 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -42,6 +42,7 @@ class Automator private constructor() { private lateinit var configurator: Configurator private lateinit var uiDevice: UiDevice private lateinit var targetContext: Context + private lateinit var uiAutomation: UiAutomation fun initialize() { if (!this::instrumentation.isInitialized) { @@ -56,6 +57,9 @@ class Automator private constructor() { if (!this::uiDevice.isInitialized) { uiDevice = UiDevice.getInstance(instrumentation) } + if (!this::uiAutomation.isInitialized) { + uiAutomation = instrumentation.uiAutomation + } } fun configure(waitForSelectorTimeout: Long) { @@ -146,6 +150,12 @@ class Automator private constructor() { return uiObjects2.map { fromUiObject2(it) } } + fun getNativeUITrees(): List { + Logger.d("getNativeUITrees()") + + return getWindowTrees(uiDevice, uiAutomation) + } + fun tap(uiSelector: UiSelector, bySelector: BySelector, index: Int) { Logger.d("tap(): $uiSelector, $bySelector") diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt index 6ea616c19..c2edb6640 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/AutomatorServer.kt @@ -3,6 +3,8 @@ package pl.leancode.patrol import pl.leancode.patrol.contracts.Contracts.ConfigureRequest import pl.leancode.patrol.contracts.Contracts.DarkModeRequest import pl.leancode.patrol.contracts.Contracts.EnterTextRequest +import pl.leancode.patrol.contracts.Contracts.GetNativeUITreeRequest +import pl.leancode.patrol.contracts.Contracts.GetNativeUITreeRespone import pl.leancode.patrol.contracts.Contracts.GetNativeViewsRequest import pl.leancode.patrol.contracts.Contracts.GetNativeViewsResponse import pl.leancode.patrol.contracts.Contracts.GetNotificationsRequest @@ -67,6 +69,11 @@ class AutomatorServer(private val automation: Automator) : NativeAutomatorServer automation.openQuickSettings() } + override fun getNativeUITree(request: GetNativeUITreeRequest): GetNativeUITreeRespone { + val trees = automation.getNativeUITrees() + return GetNativeUITreeRespone(trees) + } + override fun enableDarkMode(request: DarkModeRequest) { automation.enableDarkMode() } diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/UITreeUtils.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/UITreeUtils.kt new file mode 100644 index 000000000..f61da87f3 --- /dev/null +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/UITreeUtils.kt @@ -0,0 +1,66 @@ +package pl.leancode.patrol + +import android.app.UiAutomation +import android.os.Build +import android.view.accessibility.AccessibilityNodeInfo +import androidx.test.uiautomator.UiDevice +import pl.leancode.patrol.contracts.Contracts.NativeView + +// This function is similar to AccessibilityNodeInfoDumper.dumpWindowHierarchy() +fun getWindowTrees(uiDevice: UiDevice, uiAutomation: UiAutomation): List { + val windowRoots = getWindowRoots(uiDevice, uiAutomation) + Logger.i("Found ${windowRoots.size} windowRoots") + + return windowRoots.map { node -> fromUiAccessibilityNodeInfo(node) } +} + +// This is a private method from uiautomator.UiDevice.java +private fun getWindowRoots(uiDevice: UiDevice, uiAutomation: UiAutomation): Array { + uiDevice.waitForIdle() + val roots = mutableSetOf() + + // Start with the active window, which seems to sometimes be missing from the list returned + // by the UiAutomation. + val activeRoot: AccessibilityNodeInfo? = uiAutomation.rootInActiveWindow + if (activeRoot != null) { + roots.add(activeRoot) + } + + // Support multi-window searches for API level 21 and up. + val apiLevelActual = ( + Build.VERSION.SDK_INT + + if ("REL" == Build.VERSION.CODENAME) 0 else 1 + ) + if (apiLevelActual >= Build.VERSION_CODES.LOLLIPOP) { + for (window in uiAutomation.windows) { + val root = window.root + if (root != null) { + roots.add(root) + } + } + } + return roots.toTypedArray() +} + +private fun fromUiAccessibilityNodeInfo(obj: AccessibilityNodeInfo): NativeView { + val children = mutableListOf() + + for (i in 0 until obj.childCount) { + val child = obj.getChild(i) + if (child != null && child.isVisibleToUser) { + children.add(fromUiAccessibilityNodeInfo(child)) + } + } + + return NativeView( + className = obj.className?.toString(), + text = obj.text?.toString(), + contentDescription = obj.contentDescription?.toString(), + focused = obj.isFocused, + enabled = obj.isEnabled, + childCount = obj.childCount.toLong(), + resourceName = obj.viewIdResourceName, + applicationPackage = obj.packageName?.toString(), + children = children + ) +} diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt index 820eca3f8..bd2b43a76 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt @@ -125,6 +125,18 @@ class Contracts { val appId: String ) + data class GetNativeUITreeRequest ( + val iosInstalledApps: List? = null + ){ + fun hasIosInstalledApps(): Boolean { + return iosInstalledApps != null + } + } + + data class GetNativeUITreeRespone ( + val roots: List + ) + data class NativeView ( val className: String? = null, val text: String? = null, diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt index 617aee7cc..c553b00be 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt @@ -21,6 +21,7 @@ abstract class NativeAutomatorServer { abstract fun doublePressRecentApps() abstract fun openApp(request: Contracts.OpenAppRequest) abstract fun openQuickSettings(request: Contracts.OpenQuickSettingsRequest) + abstract fun getNativeUITree(request: Contracts.GetNativeUITreeRequest): Contracts.GetNativeUITreeRespone abstract fun getNativeViews(request: Contracts.GetNativeViewsRequest): Contracts.GetNativeViewsResponse abstract fun tap(request: Contracts.TapRequest) abstract fun doubleTap(request: Contracts.TapRequest) @@ -84,6 +85,11 @@ abstract class NativeAutomatorServer { openQuickSettings(body) Response(OK) }, + "getNativeUITree" bind POST to { + val body = json.fromJson(it.bodyString(), Contracts.GetNativeUITreeRequest::class.java) + val response = getNativeUITree(body) + Response(OK).body(json.toJson(response)) + }, "getNativeViews" bind POST to { val body = json.fromJson(it.bodyString(), Contracts.GetNativeViewsRequest::class.java) val response = getNativeViews(body) diff --git a/packages/patrol/example/devtools_options.yaml b/packages/patrol/example/devtools_options.yaml new file mode 100644 index 000000000..7e9b8c7c0 --- /dev/null +++ b/packages/patrol/example/devtools_options.yaml @@ -0,0 +1,2 @@ +extensions: + - patrol: true \ No newline at end of file diff --git a/packages/patrol/example/integration_test/common.dart b/packages/patrol/example/integration_test/common.dart index ffb049789..6cdfff56e 100644 --- a/packages/patrol/example/integration_test/common.dart +++ b/packages/patrol/example/integration_test/common.dart @@ -10,13 +10,13 @@ final _nativeAutomatorConfig = NativeAutomatorConfig( findTimeout: Duration(seconds: 20), // 10 seconds is too short for some CIs ); -Future createApp(PatrolTester $) async { +Future createApp(PatrolIntegrationTester $) async { await app_main.main(); } void patrol( String description, - Future Function(PatrolTester) callback, { + Future Function(PatrolIntegrationTester) callback, { bool? skip, NativeAutomatorConfig? nativeAutomatorConfig, LiveTestWidgetsFlutterBindingFramePolicy framePolicy = @@ -26,7 +26,6 @@ void patrol( description, config: _patrolTesterConfig, nativeAutomatorConfig: nativeAutomatorConfig ?? _nativeAutomatorConfig, - nativeAutomation: true, framePolicy: framePolicy, skip: skip, callback, diff --git a/packages/patrol/example/integration_test/internal/group_test.dart b/packages/patrol/example/integration_test/internal/group_test.dart index b938c20ba..60236f279 100644 --- a/packages/patrol/example/integration_test/internal/group_test.dart +++ b/packages/patrol/example/integration_test/internal/group_test.dart @@ -32,7 +32,7 @@ void main() { }); } -Future _testBody(PatrolTester $) async { +Future _testBody(PatrolIntegrationTester $) async { await createApp($); final testName = global_state.currentTestFullName; diff --git a/packages/patrol/example/integration_test/permissions/permissions_location_test.dart b/packages/patrol/example/integration_test/permissions/permissions_location_test.dart index d47f1b917..34d72bff6 100644 --- a/packages/patrol/example/integration_test/permissions/permissions_location_test.dart +++ b/packages/patrol/example/integration_test/permissions/permissions_location_test.dart @@ -8,7 +8,7 @@ import '../common.dart'; const _timeout = Duration(seconds: 5); // to avoid timeouts on CI // Firebase Test Lab pops out another dialog we need to handle -Future tapOkIfGoogleDialogAppears(PatrolTester $) async { +Future tapOkIfGoogleDialogAppears(PatrolIntegrationTester $) async { var listWithOkText = []; final inactivityTimer = Timer(Duration(seconds: 10), () {}); diff --git a/packages/patrol/example/integration_test/permissions/permissions_many_test.dart b/packages/patrol/example/integration_test/permissions/permissions_many_test.dart index e9d1c97b5..f0a3a3b1d 100644 --- a/packages/patrol/example/integration_test/permissions/permissions_many_test.dart +++ b/packages/patrol/example/integration_test/permissions/permissions_many_test.dart @@ -16,7 +16,7 @@ void main() { }); } -Future _requestAndGrantCameraPermission(PatrolTester $) async { +Future _requestAndGrantCameraPermission(PatrolIntegrationTester $) async { if (!await Permission.camera.isGranted) { expect($(#camera).$(#statusText).text, 'Not granted'); await $('Request camera permission').tap(); @@ -29,7 +29,9 @@ Future _requestAndGrantCameraPermission(PatrolTester $) async { expect($(#camera).$(#statusText).text, 'Granted'); } -Future _requestAndGrantMicrophonePermission(PatrolTester $) async { +Future _requestAndGrantMicrophonePermission( + PatrolIntegrationTester $, +) async { if (!await Permission.microphone.isGranted) { expect($(#microphone).$(#statusText).text, 'Not granted'); await $('Request microphone permission').tap(); @@ -42,7 +44,9 @@ Future _requestAndGrantMicrophonePermission(PatrolTester $) async { expect($(#microphone).$(#statusText).text, 'Granted'); } -Future _requestAndDenyContactsPermission(PatrolTester $) async { +Future _requestAndDenyContactsPermission( + PatrolIntegrationTester $, +) async { if (!await Permission.contacts.isGranted) { expect($(#contacts).$(#statusText).text, 'Not granted'); await $('Request contacts permission').tap(); diff --git a/packages/patrol/example/ios/Podfile.lock b/packages/patrol/example/ios/Podfile.lock index 4f53d1c17..d7cd4bbf6 100644 --- a/packages/patrol/example/ios/Podfile.lock +++ b/packages/patrol/example/ios/Podfile.lock @@ -8,8 +8,6 @@ PODS: - geolocator_apple (1.2.0): - Flutter - HTTPParserC (2.9.4) - - integration_test (0.0.1): - - Flutter - patrol (0.0.1): - Flutter - Telegraph (~> 0.30.0) @@ -26,7 +24,6 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) - - integration_test (from `.symlinks/plugins/integration_test/ios`) - patrol (from `.symlinks/plugins/patrol/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) @@ -46,8 +43,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_timezone/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/ios" - integration_test: - :path: ".symlinks/plugins/integration_test/ios" patrol: :path: ".symlinks/plugins/patrol/ios" permission_handler_apple: @@ -62,7 +57,6 @@ SPEC CHECKSUMS: flutter_timezone: ffb07bdad3c6276af8dada0f11978d8a1f8a20bb geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 HTTPParserC: aea14c3d2d4ac5beb3988781daa36dfa62e0d9ef - integration_test: 13825b8a9334a850581300559b8839134b124670 patrol: 792c0bb6cc4d552fc8b37f49266341c39e659b4d permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Telegraph: 12576b119324138e4929792af9e5a1085c2ecbc1 diff --git a/packages/patrol/example/pubspec.yaml b/packages/patrol/example/pubspec.yaml index 9bddd7701..bc23e4b01 100644 --- a/packages/patrol/example/pubspec.yaml +++ b/packages/patrol/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: none version: 1.0.0+1 environment: - sdk: '>=2.18.0 <3.0.0' - flutter: '>=3.3.0' + sdk: '>=3.2.0-0 <4.0.0' + flutter: '>=3.16.0-0' dependencies: cupertino_icons: ^1.0.5 @@ -21,8 +21,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - integration_test: - sdk: flutter leancode_lint: ^3.0.0 patrol: path: ../ diff --git a/packages/patrol/extension/devtools/.pubignore b/packages/patrol/extension/devtools/.pubignore new file mode 100644 index 000000000..dcecfeb90 --- /dev/null +++ b/packages/patrol/extension/devtools/.pubignore @@ -0,0 +1 @@ +!./build \ No newline at end of file diff --git a/packages/patrol/extension/devtools/config.yaml b/packages/patrol/extension/devtools/config.yaml new file mode 100644 index 000000000..b5ef23705 --- /dev/null +++ b/packages/patrol/extension/devtools/config.yaml @@ -0,0 +1,4 @@ +name: patrol +issueTracker: https://github.com/leancodepl/patrol/issues +version: 1.0.0 +materialIconCodePoint: '0xea4b' diff --git a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift index 741d034c6..0dc94fa45 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/Automator.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/Automator.swift @@ -1,5 +1,6 @@ #if PATROL_ENABLED import XCTest + import os class Automator { private lazy var device: XCUIDevice = { @@ -395,6 +396,29 @@ } } + func getUITreeRoots(installedApps: [String]) throws -> [NativeView] { + try runAction("getting ui tree roots") { + let foregroundApp = self.getForegroundApp(installedApps: installedApps) + let snapshot = try foregroundApp.snapshot() + return [NativeView.fromXCUIElementSnapshot(snapshot, foregroundApp.identifier)] + } + } + + private func getForegroundApp(installedApps: [String]) -> XCUIApplication { + let app = XCUIApplication() + if app.state == .runningForeground { + return app + } else { + for bundleIdentifier in installedApps { + let app = XCUIApplication(bundleIdentifier: bundleIdentifier) + if app.state == .runningForeground { + return app + } + } + return self.springboard + } + } + // MARK: Notifications func openNotifications() throws { @@ -814,7 +838,7 @@ extension NativeView { static func fromXCUIElement(_ xcuielement: XCUIElement, _ bundleId: String) -> NativeView { return NativeView( - className: String(xcuielement.elementType.rawValue), // TODO: Provide mapping for names + className: getElementTypeName(elementType: xcuielement.elementType), text: xcuielement.label, contentDescription: xcuielement.accessibilityLabel, focused: xcuielement.hasFocus, @@ -825,6 +849,22 @@ return NativeView.fromXCUIElement(child, bundleId) }) } + + static func fromXCUIElementSnapshot(_ xcuielement: XCUIElementSnapshot, _ bundleId: String) + -> NativeView + { + return NativeView( + className: getElementTypeName(elementType: xcuielement.elementType), + text: xcuielement.label, + contentDescription: "", // TODO: Separate request + focused: xcuielement.hasFocus, + enabled: xcuielement.isEnabled, + resourceName: xcuielement.identifier, + applicationPackage: bundleId, + children: xcuielement.children.map { child in + return NativeView.fromXCUIElementSnapshot(child, bundleId) + }) + } } #endif diff --git a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift index 0f67dea3a..e28e4b3a0 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift @@ -71,6 +71,12 @@ } } + func getNativeUITree(request: GetNativeUITreeRequest) throws -> GetNativeUITreeRespone { + let roots = try automator.getUITreeRoots(installedApps: request.iosInstalledApps ?? []) + + return GetNativeUITreeRespone(roots: roots) + } + func tap(request: TapRequest) throws { return try runCatching { try automator.tap( diff --git a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift index 7a7e4ac5c..a6c7987eb 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift @@ -83,6 +83,14 @@ struct GetNativeViewsRequest: Codable { var appId: String } +struct GetNativeUITreeRequest: Codable { + var iosInstalledApps: [String]? +} + +struct GetNativeUITreeRespone: Codable { + var roots: [NativeView] +} + struct NativeView: Codable { var className: String? var text: String? diff --git a/packages/patrol/ios/Classes/AutomatorServer/ElementTypeUtils.swift b/packages/patrol/ios/Classes/AutomatorServer/ElementTypeUtils.swift new file mode 100644 index 000000000..ecfe11b68 --- /dev/null +++ b/packages/patrol/ios/Classes/AutomatorServer/ElementTypeUtils.swift @@ -0,0 +1,96 @@ +#if PATROL_ENABLED + + import Foundation + import XCTest + + private let elementTypeNames = [ + XCUIElement.ElementType.any: "any", + XCUIElement.ElementType.other: "other", + XCUIElement.ElementType.application: "application", + XCUIElement.ElementType.group: "group", + XCUIElement.ElementType.window: "window", + XCUIElement.ElementType.sheet: "sheet", + XCUIElement.ElementType.drawer: "drawer", + XCUIElement.ElementType.alert: "alert", + XCUIElement.ElementType.dialog: "dialog", + XCUIElement.ElementType.button: "button", + XCUIElement.ElementType.radioButton: "radioButton", + XCUIElement.ElementType.radioGroup: "radioGroup", + XCUIElement.ElementType.checkBox: "checkBox", + XCUIElement.ElementType.disclosureTriangle: "disclosureTriangle", + XCUIElement.ElementType.popUpButton: "popUpButton", + XCUIElement.ElementType.comboBox: "comboBox", + XCUIElement.ElementType.menuButton: "menuButton", + XCUIElement.ElementType.toolbarButton: "toolbarButton", + XCUIElement.ElementType.popover: "popover", + XCUIElement.ElementType.keyboard: "keyboard", + XCUIElement.ElementType.key: "key", + XCUIElement.ElementType.navigationBar: "navigationBar", + XCUIElement.ElementType.tabBar: "tabBar", + XCUIElement.ElementType.tabGroup: "tabGroup", + XCUIElement.ElementType.toolbar: "toolbar", + XCUIElement.ElementType.statusBar: "statusBar", + XCUIElement.ElementType.table: "table", + XCUIElement.ElementType.tableRow: "tableRow", + XCUIElement.ElementType.tableColumn: "tableColumn", + XCUIElement.ElementType.outline: "outline", + XCUIElement.ElementType.outlineRow: "outlineRow", + XCUIElement.ElementType.browser: "browser", + XCUIElement.ElementType.collectionView: "collectionView", + XCUIElement.ElementType.slider: "slider", + XCUIElement.ElementType.pageIndicator: "pageIndicator", + XCUIElement.ElementType.progressIndicator: "progressIndicator", + XCUIElement.ElementType.activityIndicator: "activityIndicator", + XCUIElement.ElementType.segmentedControl: "segmentedControl", + XCUIElement.ElementType.picker: "picker", + XCUIElement.ElementType.pickerWheel: "pickerWheel", + XCUIElement.ElementType.switch: "switch", + XCUIElement.ElementType.toggle: "toggle", + XCUIElement.ElementType.link: "link", + XCUIElement.ElementType.image: "image", + XCUIElement.ElementType.icon: "icon", + XCUIElement.ElementType.searchField: "searchField", + XCUIElement.ElementType.scrollView: "scrollView", + XCUIElement.ElementType.scrollBar: "scrollBar", + XCUIElement.ElementType.staticText: "staticText", + XCUIElement.ElementType.textField: "textField", + XCUIElement.ElementType.secureTextField: "secureTextField", + XCUIElement.ElementType.datePicker: "datePicker", + XCUIElement.ElementType.textView: "textView", + XCUIElement.ElementType.menu: "menu", + XCUIElement.ElementType.menuItem: "menuItem", + XCUIElement.ElementType.menuBar: "menuBar", + XCUIElement.ElementType.menuBarItem: "menuBarItem", + XCUIElement.ElementType.map: "map", + XCUIElement.ElementType.webView: "webView", + XCUIElement.ElementType.incrementArrow: "incrementArrow", + XCUIElement.ElementType.decrementArrow: "decrementArrow", + XCUIElement.ElementType.timeline: "timeline", + XCUIElement.ElementType.ratingIndicator: "ratingIndicator", + XCUIElement.ElementType.valueIndicator: "valueIndicator", + XCUIElement.ElementType.splitGroup: "splitGroup", + XCUIElement.ElementType.splitter: "splitter", + XCUIElement.ElementType.relevanceIndicator: "relevanceIndicator", + XCUIElement.ElementType.colorWell: "colorWell", + XCUIElement.ElementType.helpTag: "helpTag", + XCUIElement.ElementType.matte: "matte", + XCUIElement.ElementType.dockItem: "dockItem", + XCUIElement.ElementType.ruler: "ruler", + XCUIElement.ElementType.rulerMarker: "rulerMarker", + XCUIElement.ElementType.grid: "grid", + XCUIElement.ElementType.levelIndicator: "levelIndicator", + XCUIElement.ElementType.cell: "cell", + XCUIElement.ElementType.layoutArea: "layoutArea", + XCUIElement.ElementType.layoutItem: "layoutItem", + XCUIElement.ElementType.handle: "handle", + XCUIElement.ElementType.stepper: "stepper", + XCUIElement.ElementType.tab: "tab", + XCUIElement.ElementType.touchBar: "touchBar", + XCUIElement.ElementType.statusItem: "statusItem", + ] + + func getElementTypeName(elementType: XCUIElement.ElementType) -> String? { + return elementTypeNames[elementType] + } + +#endif diff --git a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift index 6343bc8a8..9e40b9d9d 100644 --- a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift +++ b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift @@ -16,6 +16,7 @@ protocol NativeAutomatorServer { func doublePressRecentApps() throws func openApp(request: OpenAppRequest) throws func openQuickSettings(request: OpenQuickSettingsRequest) throws + func getNativeUITree(request: GetNativeUITreeRequest) throws -> GetNativeUITreeRespone func getNativeViews(request: GetNativeViewsRequest) throws -> GetNativeViewsResponse func tap(request: TapRequest) throws func doubleTap(request: TapRequest) throws @@ -88,6 +89,13 @@ extension NativeAutomatorServer { return HTTPResponse(.ok) } + private func getNativeUITreeHandler(request: HTTPRequest) throws -> HTTPResponse { + let requestArg = try JSONDecoder().decode(GetNativeUITreeRequest.self, from: request.body) + let response = try getNativeUITree(request: requestArg) + let body = try JSONEncoder().encode(response) + return HTTPResponse(.ok, body: body) + } + private func getNativeViewsHandler(request: HTTPRequest) throws -> HTTPResponse { let requestArg = try JSONDecoder().decode(GetNativeViewsRequest.self, from: request.body) let response = try getNativeViews(request: requestArg) @@ -277,6 +285,11 @@ extension NativeAutomatorServer { request: request, handler: openQuickSettingsHandler) } + server.route(.POST, "getNativeUITree") { + request in handleRequest( + request: request, + handler: getNativeUITreeHandler) + } server.route(.POST, "getNativeViews") { request in handleRequest( request: request, diff --git a/packages/patrol/lib/src/binding.dart b/packages/patrol/lib/src/binding.dart index 85328c4e7..0511a6ad0 100644 --- a/packages/patrol/lib/src/binding.dart +++ b/packages/patrol/lib/src/binding.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/common.dart'; -import 'package:integration_test/integration_test.dart'; import 'package:patrol/patrol.dart'; +import 'package:patrol/src/devtools_service_extensions/devtools_service_extensions.dart'; +// ignore: implementation_imports, depend_on_referenced_packages import 'package:patrol/src/global_state.dart' as global_state; import 'constants.dart' as constants; @@ -31,11 +34,14 @@ void _defaultPrintLogger(String message) { /// [PatrolBinding] submits the Dart test file name that is being currently /// executed to [PatrolAppService]. Once the name is submitted to it, that /// pending `runDartTest()` method returns. -class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { +class PatrolBinding extends LiveTestWidgetsFlutterBinding { /// Creates a new [PatrolBinding]. /// /// You most likely don't want to call it yourself. - PatrolBinding() { + PatrolBinding(NativeAutomatorConfig config) + : _serviceExtensions = DevtoolsServiceExtensions(config) { + shouldPropagateDevicePointerEvents = true; + final oldTestExceptionReporter = reportTestException; reportTestException = (details, testDescription) { final currentDartTest = _currentDartTest; @@ -101,13 +107,19 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { /// if necessary. /// /// This method is idempotent. - factory PatrolBinding.ensureInitialized() { + factory PatrolBinding.ensureInitialized(NativeAutomatorConfig config) { if (_instance == null) { - PatrolBinding(); + PatrolBinding(config); } return _instance!; } + @override + bool get overrideHttpClient => false; + + @override + bool get registerTestTextInput => false; + /// Logger used by this binding. void Function(String message) logger = _defaultPrintLogger; @@ -128,13 +140,17 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { String? _currentDartTest; - /// Keys are the test descriptions, and values are either [_success] or - /// a [Failure]. + /// Keys are the test descriptions, and values are either [_success] or a + /// [Failure]. final Map _testResults = {}; - // TODO: Remove once https://github.com/flutter/flutter/pull/108430 is available on the stable channel - @override - TestBindingEventSource get pointerEventSource => TestBindingEventSource.test; + final DevtoolsServiceExtensions _serviceExtensions; + + /// Temporary workaround for DevTools extension changing this value and not + /// resetting it. + /// + /// See https://github.com/flutter/devtools/issues/6719 + TargetPlatform? workaroundDebugDefaultTargetPlatformOverride; @override void initInstances() { @@ -142,6 +158,18 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { _instance = this; } + @override + void initServiceExtensions() { + super.initServiceExtensions(); + + logger('Register Patrol service extensions'); + + registerServiceExtension( + name: 'patrol.getNativeUITree', + callback: _serviceExtensions.getNativeUITree, + ); + } + @override Future runTest( Future Function() testBody, @@ -161,7 +189,16 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { } @override - void attachRootWidget(Widget rootWidget) { + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + final view = renderView.flutterView; + return TestViewConfiguration.fromView( + size: view.physicalSize / view.devicePixelRatio, + view: view, + ); + } + + @override + Widget wrapWithDefaultView(Widget rootWidget) { assert( (_currentDartTest != null) != (constants.hotRestartEnabled), '_currentDartTest can be null if and only if Hot Restart is enabled', @@ -169,27 +206,33 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { const testLabelEnabled = bool.fromEnvironment('PATROL_TEST_LABEL_ENABLED'); if (!testLabelEnabled || constants.hotRestartEnabled) { - super.attachRootWidget(RepaintBoundary(child: rootWidget)); + return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget)); } else { - super.attachRootWidget( + return super.wrapWithDefaultView( Stack( textDirection: TextDirection.ltr, children: [ RepaintBoundary(child: rootWidget), - // Prevents crashes when Android activity is resumed (see https://github.com/leancodepl/patrol/issues/901) + // Prevents crashes when Android activity is resumed (see + // https://github.com/leancodepl/patrol/issues/901) ExcludeSemantics( - child: Padding( - padding: EdgeInsets.only( - top: MediaQueryData.fromWindow(window).padding.top + 4, - left: 4, - ), - child: IgnorePointer( - child: Text( - _currentDartTest!, - textDirection: TextDirection.ltr, - style: const TextStyle(color: Colors.red), - ), - ), + child: Builder( + builder: (context) { + final view = View.of(context); + return Padding( + padding: EdgeInsets.only( + top: MediaQueryData.fromView(view).padding.top + 4, + left: 4, + ), + child: IgnorePointer( + child: Text( + _currentDartTest!, + textDirection: TextDirection.ltr, + style: const TextStyle(color: Colors.red), + ), + ), + ); + }, ), ), ], @@ -197,4 +240,45 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { ); } } + + @override + void reportExceptionNoticed(FlutterErrorDetails exception) { + // This override is copied from IntegrationTestWidgetsFlutterBinding. It may + // be not needed. + // + // See: https://github.com/flutter/flutter/issues/81534 + } +} + +/// Representing a failure includes the method name and the failure details. +class Failure { + /// Constructor requiring all fields during initialization. + Failure(this.methodName, this.details); + + /// The name of the test method which failed. + final String methodName; + + /// The details of the failure such as stack trace. + final String? details; + + /// Serializes the object to JSON. + String toJson() { + return json.encode({ + 'methodName': methodName, + 'details': details, + }); + } + + @override + String toString() => toJson(); + + /// Decode a JSON string to create a Failure object. + // ignore: prefer_constructors_over_static_methods + static Failure fromJsonString(String jsonString) { + final failure = json.decode(jsonString) as Map; + return Failure( + failure['methodName'] as String, + failure['details'] as String?, + ); + } } diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart index f6df39cb1..16a8c9a97 100644 --- a/packages/patrol/lib/src/common.dart +++ b/packages/patrol/lib/src/common.dart @@ -2,7 +2,6 @@ import 'dart:io' as io; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; import 'package:meta/meta.dart'; import 'package:patrol/src/binding.dart'; import 'package:patrol/src/global_state.dart' as global_state; @@ -37,9 +36,6 @@ typedef PatrolTesterCallback = Future Function(PatrolIntegrationTester $); /// }, /// ); /// ``` -/// -/// [bindingType] specifies the binding to use. [bindingType] is ignored if -/// [nativeAutomation] is false. @isTest void patrolTest( String description, @@ -51,43 +47,17 @@ void patrolTest( dynamic tags, finders.PatrolTesterConfig config = const finders.PatrolTesterConfig(), NativeAutomatorConfig nativeAutomatorConfig = const NativeAutomatorConfig(), - bool nativeAutomation = false, - BindingType bindingType = BindingType.patrol, LiveTestWidgetsFlutterBindingFramePolicy framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fadePointers, }) { - NativeAutomator? automator; + NativeAutomator automator; PatrolBinding? patrolBinding; - if (!nativeAutomation) { - debugPrint(''' -╔════════════════════════════════════════════════════════════════════════════════════╗ -║ In next major release, patrolTest method will be intended for UI tests only ║ -║ If you want to use Patrol in your widget tests, use patrol_finders package. ║ -║ ║ -║ For more information, see https://patrol.leancode.co/patrol-finders-release ║ -╚════════════════════════════════════════════════════════════════════════════════════╝ -'''); - } - - if (nativeAutomation) { - switch (bindingType) { - case BindingType.patrol: - automator = NativeAutomator(config: nativeAutomatorConfig); - - patrolBinding = PatrolBinding.ensureInitialized(); - patrolBinding.framePolicy = framePolicy; - break; - case BindingType.integrationTest: - IntegrationTestWidgetsFlutterBinding.ensureInitialized().framePolicy = - framePolicy; - - break; - case BindingType.none: - break; - } - } + automator = NativeAutomator(config: nativeAutomatorConfig); + final binding = + patrolBinding = PatrolBinding.ensureInitialized(nativeAutomatorConfig) + ..framePolicy = framePolicy; testWidgets( description, @@ -130,7 +100,7 @@ void patrolTest( // See https://github.com/leancodepl/patrol/issues/1474 }; } - await automator?.configure(); + await automator.configure(); final patrolTester = PatrolIntegrationTester( tester: widgetTester, @@ -143,6 +113,9 @@ void patrolTest( final waitSeconds = const int.fromEnvironment('PATROL_WAIT'); final waitDuration = Duration(seconds: waitSeconds); + debugDefaultTargetPlatformOverride = + binding.workaroundDebugDefaultTargetPlatformOverride; + if (waitDuration > Duration.zero) { final stopwatch = Stopwatch()..start(); await Future.doWhile(() async { diff --git a/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart b/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart index c3c2947cc..3297d6f1f 100644 --- a/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart +++ b/packages/patrol/lib/src/custom_finders/patrol_integration_tester.dart @@ -1,15 +1,6 @@ import 'package:patrol/src/native/native_automator.dart'; import 'package:patrol_finders/patrol_finders.dart' as finders; -/// This typedef help us to avoid breaking changes. -@Deprecated( - ''' - PatrolTester will be accessible only in patrol_finders package. - Use PatrolIntegrationTester in patrolTest callback - ''', -) -typedef PatrolTester = PatrolIntegrationTester; - /// PatrolIntegrationTester extends the capabilities of [finders.PatrolTester] /// with the ability to interact with native platform features via [native]. class PatrolIntegrationTester extends finders.PatrolTester { @@ -23,18 +14,8 @@ class PatrolIntegrationTester extends finders.PatrolTester { /// Native automator that allows for interaction with OS the app is running /// on. /// - /// TODO This field will not be nullable or will be removed - final NativeAutomator? nativeAutomator; + final NativeAutomator nativeAutomator; - /// Shorthand for [nativeAutomator]. Throws if [nativeAutomator] is null, - /// which is the case if it wasn't initialized. - NativeAutomator get native { - assert( - nativeAutomator != null, - 'NativeAutomator is not initialized. Make sure you passed ' - "`nativeAutomation: true` to patrolTest(), and that you're *not* " - 'initializing any bindings in your test.', - ); - return nativeAutomator!; - } + /// Shorthand for [nativeAutomator]. + NativeAutomator get native => nativeAutomator; } diff --git a/packages/patrol/lib/src/devtools_service_extensions/devtools_service_extensions.dart b/packages/patrol/lib/src/devtools_service_extensions/devtools_service_extensions.dart new file mode 100644 index 000000000..86e6d549f --- /dev/null +++ b/packages/patrol/lib/src/devtools_service_extensions/devtools_service_extensions.dart @@ -0,0 +1,57 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:convert'; +import 'package:flutter/widgets.dart'; +import 'package:http/http.dart' as http; +import 'package:patrol/src/native/contracts/contracts.dart'; +import 'package:patrol/src/native/contracts/native_automator_client.dart'; +import 'package:patrol/src/native/native_automator.dart'; + +class DevtoolsServiceExtensions { + DevtoolsServiceExtensions(NativeAutomatorConfig config) { + _client = NativeAutomatorClient( + http.Client(), + Uri.http('${config.host}:${config.port}'), + timeout: config.connectionTimeout, + ); + + _iosInstalledApps = config.iosInstalledApps.isNotEmpty + ? (jsonDecode(config.iosInstalledApps) as List).cast() + : null; + } + + late final List? _iosInstalledApps; + late final NativeAutomatorClient _client; + + Future> getNativeUITree(Map parameters) { + return _wrapRequest('getNativeUITree', () async { + final res = await _client.getNativeUITree( + GetNativeUITreeRequest(iosInstalledApps: _iosInstalledApps), + ); + return res.toJson(); + }); + } + + Future> _wrapRequest( + String name, + Future> Function() callback, + ) async { + try { + debugPrint('Start $name'); + + final res = await callback(); + + debugPrint('End $name'); + + return { + 'result': res, + 'success': true, + }; + } catch (e, t) { + return { + 'result': '$e $t', + 'success': false, + }; + } + } +} diff --git a/packages/patrol/lib/src/native/contracts/contracts.dart b/packages/patrol/lib/src/native/contracts/contracts.dart index 27fb69c01..b8106b9b4 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.dart @@ -258,6 +258,44 @@ class GetNativeViewsRequest with EquatableMixin { ]; } +@JsonSerializable() +class GetNativeUITreeRequest with EquatableMixin { + GetNativeUITreeRequest({ + this.iosInstalledApps, + }); + + factory GetNativeUITreeRequest.fromJson(Map json) => + _$GetNativeUITreeRequestFromJson(json); + + final List? iosInstalledApps; + + Map toJson() => _$GetNativeUITreeRequestToJson(this); + + @override + List get props => [ + iosInstalledApps, + ]; +} + +@JsonSerializable() +class GetNativeUITreeRespone with EquatableMixin { + GetNativeUITreeRespone({ + required this.roots, + }); + + factory GetNativeUITreeRespone.fromJson(Map json) => + _$GetNativeUITreeResponeFromJson(json); + + final List roots; + + Map toJson() => _$GetNativeUITreeResponeToJson(this); + + @override + List get props => [ + roots, + ]; +} + @JsonSerializable() class NativeView with EquatableMixin { NativeView({ diff --git a/packages/patrol/lib/src/native/contracts/contracts.g.dart b/packages/patrol/lib/src/native/contracts/contracts.g.dart index 02a6c767f..3f6ead636 100644 --- a/packages/patrol/lib/src/native/contracts/contracts.g.dart +++ b/packages/patrol/lib/src/native/contracts/contracts.g.dart @@ -141,6 +141,34 @@ Map _$GetNativeViewsRequestToJson( 'appId': instance.appId, }; +GetNativeUITreeRequest _$GetNativeUITreeRequestFromJson( + Map json) => + GetNativeUITreeRequest( + iosInstalledApps: (json['iosInstalledApps'] as List?) + ?.map((e) => e as String) + .toList(), + ); + +Map _$GetNativeUITreeRequestToJson( + GetNativeUITreeRequest instance) => + { + 'iosInstalledApps': instance.iosInstalledApps, + }; + +GetNativeUITreeRespone _$GetNativeUITreeResponeFromJson( + Map json) => + GetNativeUITreeRespone( + roots: (json['roots'] as List) + .map((e) => NativeView.fromJson(e as Map)) + .toList(), + ); + +Map _$GetNativeUITreeResponeToJson( + GetNativeUITreeRespone instance) => + { + 'roots': instance.roots, + }; + NativeView _$NativeViewFromJson(Map json) => NativeView( className: json['className'] as String?, text: json['text'] as String?, diff --git a/packages/patrol/lib/src/native/contracts/native_automator_client.dart b/packages/patrol/lib/src/native/contracts/native_automator_client.dart index ae1c10d6a..aa0433a40 100644 --- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart +++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart @@ -96,6 +96,16 @@ class NativeAutomatorClient { ); } + Future getNativeUITree( + GetNativeUITreeRequest request, + ) async { + final json = await _sendRequest( + 'getNativeUITree', + request.toJson(), + ); + return GetNativeUITreeRespone.fromJson(json); + } + Future getNativeViews( GetNativeViewsRequest request, ) async { diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index cde6bfaf5..8322d4e7e 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -2,11 +2,9 @@ import 'dart:io' as io; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; -import 'package:integration_test/integration_test.dart'; import 'package:meta/meta.dart'; -import 'package:patrol/src/binding.dart'; -import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/contracts/contracts.dart' as contracts; +import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/contracts/native_automator_client.dart'; /// Thrown when a native action fails. @@ -21,18 +19,6 @@ class PatrolActionException implements Exception { String toString() => 'Patrol action failed: $message'; } -/// Bindings available to use with [NativeAutomator]. -enum BindingType { - /// Initialize [PatrolBinding]. - patrol, - - /// Initializes [IntegrationTestWidgetsFlutterBinding] - integrationTest, - - /// Doesn't initialize any binding. - none, -} - /// Specifies how the OS keyboard should behave when using /// [NativeAutomator.enterText] and [NativeAutomator.enterTextByIndex]. enum KeyboardBehavior { @@ -80,6 +66,8 @@ class NativeAutomatorConfig { defaultValue: '8081', ), this.packageName = const String.fromEnvironment('PATROL_APP_PACKAGE_NAME'), + this.iosInstalledApps = + const String.fromEnvironment('PATROL_IOS_INSTALLED_APPS'), this.bundleId = const String.fromEnvironment('PATROL_APP_BUNDLE_ID'), this.androidAppName = const String.fromEnvironment('PATROL_ANDROID_APP_NAME'), @@ -90,6 +78,12 @@ class NativeAutomatorConfig { this.logger = _defaultPrintLogger, }); + /// Apps installed on the iOS simulator. + /// + /// This is needed for purpose of native view inspection in the Patrol + /// DevTools extension. + final String iosInstalledApps; + /// Host on which Patrol server instrumentation is running. final String host; diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index e7b44d77e..1d0d7e1d7 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -8,19 +8,16 @@ repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues environment: - sdk: '>=2.18.0 <4.0.0' - flutter: '>=3.3.0' + sdk: '>=3.1.0 <4.0.0' # TODO: https://github.com/leancodepl/patrol/issues/1893 + flutter: '>=3.16.0-0' dependencies: equatable: ^2.0.3 - fixnum: ^1.0.1 flutter: sdk: flutter flutter_test: sdk: flutter http: '>=0.13.5 <2.0.0' - integration_test: - sdk: flutter json_annotation: ^4.6.0 meta: ^1.7.0 path: ^1.8.2 diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index 15715cb51..6b9c266ec 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +- Add support for Patrol 3.0 and its DevTools extension (#1829) + - Automatically open DevTools when running `patrol develop`. This behavior can + be disabled by passing `--no-open-devtools` flag. + ## 2.2.2 - Fix parsing `--dart-defines` when a value contains a comma (#1845) diff --git a/packages/patrol_cli/lib/src/commands/develop.dart b/packages/patrol_cli/lib/src/commands/develop.dart index 03ff86857..b3af87bc1 100644 --- a/packages/patrol_cli/lib/src/commands/develop.dart +++ b/packages/patrol_cli/lib/src/commands/develop.dart @@ -49,6 +49,12 @@ class DevelopCommand extends PatrolCommand { usesAndroidOptions(); usesIOSOptions(); + + argParser.addFlag( + 'open-devtools', + help: 'Automatically open Patrol extension in DevTools when ready.', + defaultsTo: true, + ); } final DeviceFinder _deviceFinder; @@ -112,6 +118,12 @@ class DevelopCommand extends PatrolCommand { final displayLabel = boolArg('label'); final uninstall = boolArg('uninstall'); + String? iOSInstalledAppsEnvVariable; + if (device.targetPlatform == TargetPlatform.iOS) { + iOSInstalledAppsEnvVariable = + await _iosTestBackend.getInstalledAppsEnvVariable(device.id); + } + final customDartDefines = { ..._dartDefinesReader.fromFile(), ..._dartDefinesReader.fromCli(args: stringsArg('dart-define')), @@ -125,7 +137,10 @@ class DevelopCommand extends PatrolCommand { 'INTEGRATION_TEST_SHOULD_REPORT_RESULTS_TO_NATIVE': 'false', 'PATROL_TEST_LABEL_ENABLED': displayLabel.toString(), // develop-specific - ...{'PATROL_HOT_RESTART': 'true'}, + ...{ + 'PATROL_HOT_RESTART': 'true', + 'PATROL_IOS_INSTALLED_APPS': iOSInstalledAppsEnvVariable, + }, }.withNullsRemoved(); final dartDefines = {...customDartDefines, ...internalDartDefines}; @@ -170,6 +185,7 @@ class DevelopCommand extends PatrolCommand { iosOpts, uninstall: uninstall, device: device, + openDevtools: boolArg('open-devtools'), ); return 0; // for now, all exit codes are 0 @@ -245,6 +261,7 @@ class DevelopCommand extends PatrolCommand { IOSAppOptions iosOpts, { required bool uninstall, required Device device, + required bool openDevtools, }) async { Future Function() action; Future Function()? finalizer; @@ -282,6 +299,7 @@ class DevelopCommand extends PatrolCommand { target: flutterOpts.target, appId: appId, dartDefines: flutterOpts.dartDefines, + openDevtools: openDevtools, ); await future; diff --git a/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart b/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart index 4ca7cda00..f80a1cfb9 100644 --- a/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart +++ b/packages/patrol_cli/lib/src/crossplatform/flutter_tool.dart @@ -6,16 +6,19 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' show basename; import 'package:patrol_cli/src/base/logger.dart'; import 'package:patrol_cli/src/base/process.dart'; +import 'package:platform/platform.dart'; import 'package:process/process.dart'; class FlutterTool { FlutterTool({ required Stream> stdin, required ProcessManager processManager, + required Platform platform, required DisposeScope parentDisposeScope, required Logger logger, }) : _stdin = stdin, _processManager = processManager, + _platform = platform, _disposeScope = DisposeScope(), _logger = logger { _disposeScope.disposedBy(parentDisposeScope); @@ -23,6 +26,7 @@ class FlutterTool { final Stream> _stdin; final ProcessManager _processManager; + final Platform _platform; final DisposeScope _disposeScope; final Logger _logger; @@ -35,6 +39,7 @@ class FlutterTool { required String target, required String? appId, required Map dartDefines, + required bool openDevtools, }) async { if (io.stdin.hasTerminal) { _enableInteractiveMode(); @@ -47,18 +52,24 @@ class FlutterTool { target: target, appId: appId, dartDefines: dartDefines, + openBrowser: openDevtools, ), ]); } /// Attaches to the running app. Returns a [Future] that completes when the /// connection is ready. + /// + /// If [openBrowser] is true, Dart DevTools (with Patrol extension page + /// selected) will be automatically opened in the browser once DevTools URL is + /// printed by Flutter CLI. @visibleForTesting Future attach({ required String deviceId, required String target, required String? appId, required Map dartDefines, + required bool openBrowser, }) async { await _disposeScope.run((scope) async { final process = await _processManager.start( @@ -111,6 +122,13 @@ class FlutterTool { } completer.complete(); } + + if (openBrowser && + line.startsWith('The Flutter DevTools debugger and profiler')) { + final url = _getDevtoolsUrl(line); + unawaited(_openDevtoolsPage(url)); + } + _logger.detail('\t: $line'); }).disposedBy(scope); @@ -187,4 +205,28 @@ class FlutterTool { _logger.detail('Interactive shell mode enabled.'); } + + Future _openDevtoolsPage(String url) async { + _logger.success('Patrol DevTools extension is available at $url'); + + io.Process? process; + switch (_platform.operatingSystem) { + case Platform.macOS: + process = await _processManager.start(['open', url]); + break; + case Platform.windows: + process = await _processManager.start(['start', url]); + break; + case Platform.linux: + process = await _processManager.start(['xdg-open', url]); + } + + await process?.exitCode; + } + + String _getDevtoolsUrl(String line) { + final startIndex = line.indexOf('http'); + final url = line.substring(startIndex); + return url.replaceAllMapped('?uri=', (_) => '/patrol_ext?uri='); + } } diff --git a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart index 9e1edec3c..23f24098b 100644 --- a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart +++ b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart @@ -340,4 +340,20 @@ class IOSTestBackend { _logger.detail('Assuming SDK version $sdkVersion for $platform'); return sdkVersion; } + + Future getInstalledAppsEnvVariable(String deviceId) async { + final processResult = await _processManager.run( + ['xcrun', 'simctl', 'listapps', deviceId], + runInShell: true, + ); + + const lineSplitter = LineSplitter(); + final ids = lineSplitter + .convert(processResult.stdOut) + .where((line) => line.contains('CFBundleIdentifier =')) + .map((e) => e.substring(e.indexOf('"') + 1, e.lastIndexOf('"'))) + .toList(); + + return jsonEncode(ids); + } } diff --git a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart index 74780d63b..76297b9c1 100644 --- a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart +++ b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart @@ -158,6 +158,7 @@ class PatrolCommandRunner extends CompletionCommandRunner { flutterTool: FlutterTool( stdin: stdin, processManager: _processManager, + platform: _platform, parentDisposeScope: _disposeScope, logger: _logger, ), diff --git a/packages/patrol_cli/lib/src/test_bundler.dart b/packages/patrol_cli/lib/src/test_bundler.dart index a6968430c..bc8674c7a 100644 --- a/packages/patrol_cli/lib/src/test_bundler.dart +++ b/packages/patrol_cli/lib/src/test_bundler.dart @@ -70,7 +70,7 @@ Future main() async { final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig()); await nativeAutomator.initialize(); - final binding = PatrolBinding.ensureInitialized(); + final binding = PatrolBinding.ensureInitialized(NativeAutomatorConfig()); final testExplorationCompleter = Completer(); // A special test to explore the hierarchy of groups and tests. This is a hack @@ -128,6 +128,7 @@ ${generateGroupsCode(testFilePaths).split('\n').map((e) => ' $e').join('\n')} final contents = ''' // ignore_for_file: type=lint, invalid_use_of_internal_member +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol/patrol.dart'; @@ -138,7 +139,9 @@ ${generateImports([testFilePath])} Future main() async { final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig()); await nativeAutomator.initialize(); - PatrolBinding.ensureInitialized(); + PatrolBinding.ensureInitialized(NativeAutomatorConfig()) + ..workaroundDebugDefaultTargetPlatformOverride = + debugDefaultTargetPlatformOverride; // START: GENERATED TEST GROUPS ${generateGroupsCode([testFilePath]).split('\n').map((e) => ' $e').join('\n')} diff --git a/packages/patrol_cli/pubspec.yaml b/packages/patrol_cli/pubspec.yaml index dca4ea7b3..565784c78 100644 --- a/packages/patrol_cli/pubspec.yaml +++ b/packages/patrol_cli/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.1.0 <4.0.0' # TODO: https://github.com/leancodepl/patrol/issues/1893 dependencies: adb: ^0.2.5 @@ -31,7 +31,7 @@ dependencies: uuid: ^3.0.7 yaml: ^3.1.1 dev_dependencies: - build_runner: ^2.3.3 + build_runner: ^2.4.6 fake_async: ^1.3.1 leancode_lint: ^3.0.0 mocktail: ^0.3.0 diff --git a/packages/patrol_cli/test/crossplatform/flutter_tool_test.dart b/packages/patrol_cli/test/crossplatform/flutter_tool_test.dart index 8861766e5..7b6ad2b01 100644 --- a/packages/patrol_cli/test/crossplatform/flutter_tool_test.dart +++ b/packages/patrol_cli/test/crossplatform/flutter_tool_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dispose_scope/dispose_scope.dart'; import 'package:mocktail/mocktail.dart'; import 'package:patrol_cli/src/crossplatform/flutter_tool.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import '../src/mocks.dart'; @@ -10,16 +11,19 @@ import '../src/mocks.dart'; void main() { late FlutterTool flutterTool; late MockProcessManager processManager; + late Platform platform; setUp(() { final disposeScope = DisposeScope(); final stdin = StreamController>(); processManager = MockProcessManager(); + platform = FakePlatform(); flutterTool = FlutterTool( logger: MockLogger(), parentDisposeScope: disposeScope, processManager: processManager, + platform: platform, stdin: stdin.stream, ); }); @@ -43,6 +47,7 @@ void main() { target: 'target', appId: 'appId', dartDefines: {}, + openBrowser: false, ); verify( diff --git a/packages/patrol_cli/test/src/fakes.dart b/packages/patrol_cli/test/src/fakes.dart index 5fed44e28..128a31e32 100644 --- a/packages/patrol_cli/test/src/fakes.dart +++ b/packages/patrol_cli/test/src/fakes.dart @@ -1,10 +1,6 @@ -import 'dart:io' as io; - import 'package:mocktail/mocktail.dart'; import 'package:platform/platform.dart'; -class FakeProcessResult extends Fake implements io.ProcessResult {} - void setUpFakes() { registerFallbackValue(Uri()); } diff --git a/packages/patrol_devtools_extension/.gitignore b/packages/patrol_devtools_extension/.gitignore new file mode 100644 index 000000000..24476c5d1 --- /dev/null +++ b/packages/patrol_devtools_extension/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/patrol_devtools_extension/.metadata b/packages/patrol_devtools_extension/.metadata new file mode 100644 index 000000000..bd1207f92 --- /dev/null +++ b/packages/patrol_devtools_extension/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: web + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/patrol_devtools_extension/README.md b/packages/patrol_devtools_extension/README.md new file mode 100644 index 000000000..791493c22 --- /dev/null +++ b/packages/patrol_devtools_extension/README.md @@ -0,0 +1,16 @@ +# patrol_devtools_extension + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/patrol_devtools_extension/analysis_options.yaml b/packages/patrol_devtools_extension/analysis_options.yaml new file mode 100644 index 000000000..ee7809aa4 --- /dev/null +++ b/packages/patrol_devtools_extension/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:leancode_lint/analysis_options.yaml + +analyzer: + exclude: + - lib/**/*.g.dart diff --git a/packages/patrol_devtools_extension/lib/api/contracts.dart b/packages/patrol_devtools_extension/lib/api/contracts.dart new file mode 100644 index 000000000..6bd6bd882 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/api/contracts.dart @@ -0,0 +1,626 @@ +// +// Generated code. Do not modify. +// source: schema.dart +// +// ignore_for_file: public_member_api_docs + +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'contracts.g.dart'; + +enum GroupEntryType { + @JsonValue('group') + group, + @JsonValue('test') + test +} + +enum RunDartTestResponseResult { + @JsonValue('success') + success, + @JsonValue('skipped') + skipped, + @JsonValue('failure') + failure +} + +enum KeyboardBehavior { + @JsonValue('showAndDismiss') + showAndDismiss, + @JsonValue('alternative') + alternative +} + +enum HandlePermissionRequestCode { + @JsonValue('whileUsing') + whileUsing, + @JsonValue('onlyThisTime') + onlyThisTime, + @JsonValue('denied') + denied +} + +enum SetLocationAccuracyRequestLocationAccuracy { + @JsonValue('coarse') + coarse, + @JsonValue('fine') + fine +} + +@JsonSerializable() +class DartGroupEntry with EquatableMixin { + DartGroupEntry({ + required this.name, + required this.type, + required this.entries, + }); + + factory DartGroupEntry.fromJson(Map json) => + _$DartGroupEntryFromJson(json); + + final String name; + final GroupEntryType type; + final List entries; + + Map toJson() => _$DartGroupEntryToJson(this); + + @override + List get props => [ + name, + type, + entries, + ]; +} + +@JsonSerializable() +class ListDartTestsResponse with EquatableMixin { + ListDartTestsResponse({ + required this.group, + }); + + factory ListDartTestsResponse.fromJson(Map json) => + _$ListDartTestsResponseFromJson(json); + + final DartGroupEntry group; + + Map toJson() => _$ListDartTestsResponseToJson(this); + + @override + List get props => [ + group, + ]; +} + +@JsonSerializable() +class RunDartTestRequest with EquatableMixin { + RunDartTestRequest({ + required this.name, + }); + + factory RunDartTestRequest.fromJson(Map json) => + _$RunDartTestRequestFromJson(json); + + final String name; + + Map toJson() => _$RunDartTestRequestToJson(this); + + @override + List get props => [ + name, + ]; +} + +@JsonSerializable() +class RunDartTestResponse with EquatableMixin { + RunDartTestResponse({ + required this.result, + this.details, + }); + + factory RunDartTestResponse.fromJson(Map json) => + _$RunDartTestResponseFromJson(json); + + final RunDartTestResponseResult result; + final String? details; + + Map toJson() => _$RunDartTestResponseToJson(this); + + @override + List get props => [ + result, + details, + ]; +} + +@JsonSerializable() +class ConfigureRequest with EquatableMixin { + ConfigureRequest({ + required this.findTimeoutMillis, + }); + + factory ConfigureRequest.fromJson(Map json) => + _$ConfigureRequestFromJson(json); + + final int findTimeoutMillis; + + Map toJson() => _$ConfigureRequestToJson(this); + + @override + List get props => [ + findTimeoutMillis, + ]; +} + +@JsonSerializable() +class OpenAppRequest with EquatableMixin { + OpenAppRequest({ + required this.appId, + }); + + factory OpenAppRequest.fromJson(Map json) => + _$OpenAppRequestFromJson(json); + + final String appId; + + Map toJson() => _$OpenAppRequestToJson(this); + + @override + List get props => [ + appId, + ]; +} + +@JsonSerializable() +class OpenQuickSettingsRequest with EquatableMixin { + OpenQuickSettingsRequest(); + + factory OpenQuickSettingsRequest.fromJson(Map json) => + _$OpenQuickSettingsRequestFromJson(json); + + Map toJson() => _$OpenQuickSettingsRequestToJson(this); + + @override + List get props => const []; +} + +@JsonSerializable() +class Selector with EquatableMixin { + Selector({ + this.text, + this.textStartsWith, + this.textContains, + this.className, + this.contentDescription, + this.contentDescriptionStartsWith, + this.contentDescriptionContains, + this.resourceId, + this.instance, + this.enabled, + this.focused, + this.pkg, + }); + + factory Selector.fromJson(Map json) => + _$SelectorFromJson(json); + + final String? text; + final String? textStartsWith; + final String? textContains; + final String? className; + final String? contentDescription; + final String? contentDescriptionStartsWith; + final String? contentDescriptionContains; + final String? resourceId; + final int? instance; + final bool? enabled; + final bool? focused; + final String? pkg; + + Map toJson() => _$SelectorToJson(this); + + @override + List get props => [ + text, + textStartsWith, + textContains, + className, + contentDescription, + contentDescriptionStartsWith, + contentDescriptionContains, + resourceId, + instance, + enabled, + focused, + pkg, + ]; +} + +@JsonSerializable() +class GetNativeViewsRequest with EquatableMixin { + GetNativeViewsRequest({ + required this.selector, + required this.appId, + }); + + factory GetNativeViewsRequest.fromJson(Map json) => + _$GetNativeViewsRequestFromJson(json); + + final Selector selector; + final String appId; + + Map toJson() => _$GetNativeViewsRequestToJson(this); + + @override + List get props => [ + selector, + appId, + ]; +} + +@JsonSerializable() +class GetNativeUITreeRespone with EquatableMixin { + GetNativeUITreeRespone({ + required this.roots, + }); + + factory GetNativeUITreeRespone.fromJson(Map json) => + _$GetNativeUITreeResponeFromJson(json); + + final List roots; + + Map toJson() => _$GetNativeUITreeResponeToJson(this); + + @override + List get props => [ + roots, + ]; +} + +@JsonSerializable() +class NativeView with EquatableMixin { + NativeView({ + this.className, + this.text, + this.contentDescription, + required this.focused, + required this.enabled, + this.childCount, + this.resourceName, + this.applicationPackage, + required this.children, + }); + + factory NativeView.fromJson(Map json) => + _$NativeViewFromJson(json); + + final String? className; + final String? text; + final String? contentDescription; + final bool focused; + final bool enabled; + final int? childCount; + //TODO: rename to "resourceId" for consistency + final String? resourceName; + //TODO: rename to "pkg" for consistency + final String? applicationPackage; + final List children; + + Map toJson() => _$NativeViewToJson(this); + + @override + List get props => [ + className, + text, + contentDescription, + focused, + enabled, + childCount, + resourceName, + applicationPackage, + children, + ]; +} + +@JsonSerializable() +class GetNativeViewsResponse with EquatableMixin { + GetNativeViewsResponse({ + required this.nativeViews, + }); + + factory GetNativeViewsResponse.fromJson(Map json) => + _$GetNativeViewsResponseFromJson(json); + + final List nativeViews; + + Map toJson() => _$GetNativeViewsResponseToJson(this); + + @override + List get props => [ + nativeViews, + ]; +} + +@JsonSerializable() +class TapRequest with EquatableMixin { + TapRequest({ + required this.selector, + required this.appId, + }); + + factory TapRequest.fromJson(Map json) => + _$TapRequestFromJson(json); + + final Selector selector; + final String appId; + + Map toJson() => _$TapRequestToJson(this); + + @override + List get props => [ + selector, + appId, + ]; +} + +@JsonSerializable() +class EnterTextRequest with EquatableMixin { + EnterTextRequest({ + required this.data, + required this.appId, + this.index, + this.selector, + required this.keyboardBehavior, + }); + + factory EnterTextRequest.fromJson(Map json) => + _$EnterTextRequestFromJson(json); + + final String data; + final String appId; + final int? index; + final Selector? selector; + final KeyboardBehavior keyboardBehavior; + + Map toJson() => _$EnterTextRequestToJson(this); + + @override + List get props => [ + data, + appId, + index, + selector, + keyboardBehavior, + ]; +} + +@JsonSerializable() +class SwipeRequest with EquatableMixin { + SwipeRequest({ + required this.startX, + required this.startY, + required this.endX, + required this.endY, + required this.steps, + }); + + factory SwipeRequest.fromJson(Map json) => + _$SwipeRequestFromJson(json); + + final double startX; + final double startY; + final double endX; + final double endY; + final int steps; + + Map toJson() => _$SwipeRequestToJson(this); + + @override + List get props => [ + startX, + startY, + endX, + endY, + steps, + ]; +} + +@JsonSerializable() +class WaitUntilVisibleRequest with EquatableMixin { + WaitUntilVisibleRequest({ + required this.selector, + required this.appId, + }); + + factory WaitUntilVisibleRequest.fromJson(Map json) => + _$WaitUntilVisibleRequestFromJson(json); + + final Selector selector; + final String appId; + + Map toJson() => _$WaitUntilVisibleRequestToJson(this); + + @override + List get props => [ + selector, + appId, + ]; +} + +@JsonSerializable() +class DarkModeRequest with EquatableMixin { + DarkModeRequest({ + required this.appId, + }); + + factory DarkModeRequest.fromJson(Map json) => + _$DarkModeRequestFromJson(json); + + final String appId; + + Map toJson() => _$DarkModeRequestToJson(this); + + @override + List get props => [ + appId, + ]; +} + +@JsonSerializable() +class Notification with EquatableMixin { + Notification({ + this.appName, + required this.title, + required this.content, + this.raw, + }); + + factory Notification.fromJson(Map json) => + _$NotificationFromJson(json); + + final String? appName; + final String title; + final String content; + final String? raw; + + Map toJson() => _$NotificationToJson(this); + + @override + List get props => [ + appName, + title, + content, + raw, + ]; +} + +@JsonSerializable() +class GetNotificationsResponse with EquatableMixin { + GetNotificationsResponse({ + required this.notifications, + }); + + factory GetNotificationsResponse.fromJson(Map json) => + _$GetNotificationsResponseFromJson(json); + + final List notifications; + + Map toJson() => _$GetNotificationsResponseToJson(this); + + @override + List get props => [ + notifications, + ]; +} + +@JsonSerializable() +class GetNotificationsRequest with EquatableMixin { + GetNotificationsRequest(); + + factory GetNotificationsRequest.fromJson(Map json) => + _$GetNotificationsRequestFromJson(json); + + Map toJson() => _$GetNotificationsRequestToJson(this); + + @override + List get props => const []; +} + +@JsonSerializable() +class TapOnNotificationRequest with EquatableMixin { + TapOnNotificationRequest({ + this.index, + this.selector, + }); + + factory TapOnNotificationRequest.fromJson(Map json) => + _$TapOnNotificationRequestFromJson(json); + + final int? index; + final Selector? selector; + + Map toJson() => _$TapOnNotificationRequestToJson(this); + + @override + List get props => [ + index, + selector, + ]; +} + +@JsonSerializable() +class PermissionDialogVisibleResponse with EquatableMixin { + PermissionDialogVisibleResponse({ + required this.visible, + }); + + factory PermissionDialogVisibleResponse.fromJson(Map json) => + _$PermissionDialogVisibleResponseFromJson(json); + + final bool visible; + + Map toJson() => + _$PermissionDialogVisibleResponseToJson(this); + + @override + List get props => [ + visible, + ]; +} + +@JsonSerializable() +class PermissionDialogVisibleRequest with EquatableMixin { + PermissionDialogVisibleRequest({ + required this.timeoutMillis, + }); + + factory PermissionDialogVisibleRequest.fromJson(Map json) => + _$PermissionDialogVisibleRequestFromJson(json); + + final int timeoutMillis; + + Map toJson() => _$PermissionDialogVisibleRequestToJson(this); + + @override + List get props => [ + timeoutMillis, + ]; +} + +@JsonSerializable() +class HandlePermissionRequest with EquatableMixin { + HandlePermissionRequest({ + required this.code, + }); + + factory HandlePermissionRequest.fromJson(Map json) => + _$HandlePermissionRequestFromJson(json); + + final HandlePermissionRequestCode code; + + Map toJson() => _$HandlePermissionRequestToJson(this); + + @override + List get props => [ + code, + ]; +} + +@JsonSerializable() +class SetLocationAccuracyRequest with EquatableMixin { + SetLocationAccuracyRequest({ + required this.locationAccuracy, + }); + + factory SetLocationAccuracyRequest.fromJson(Map json) => + _$SetLocationAccuracyRequestFromJson(json); + + final SetLocationAccuracyRequestLocationAccuracy locationAccuracy; + + Map toJson() => _$SetLocationAccuracyRequestToJson(this); + + @override + List get props => [ + locationAccuracy, + ]; +} diff --git a/packages/patrol_devtools_extension/lib/api/contracts.g.dart b/packages/patrol_devtools_extension/lib/api/contracts.g.dart new file mode 100644 index 000000000..22c6efd1d --- /dev/null +++ b/packages/patrol_devtools_extension/lib/api/contracts.g.dart @@ -0,0 +1,390 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'contracts.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DartGroupEntry _$DartGroupEntryFromJson(Map json) => + DartGroupEntry( + name: json['name'] as String, + type: $enumDecode(_$GroupEntryTypeEnumMap, json['type']), + entries: (json['entries'] as List) + .map((e) => DartGroupEntry.fromJson(e as Map)) + .toList(), + ); + +Map _$DartGroupEntryToJson(DartGroupEntry instance) => + { + 'name': instance.name, + 'type': _$GroupEntryTypeEnumMap[instance.type]!, + 'entries': instance.entries, + }; + +const _$GroupEntryTypeEnumMap = { + GroupEntryType.group: 'group', + GroupEntryType.test: 'test', +}; + +ListDartTestsResponse _$ListDartTestsResponseFromJson( + Map json) => + ListDartTestsResponse( + group: DartGroupEntry.fromJson(json['group'] as Map), + ); + +Map _$ListDartTestsResponseToJson( + ListDartTestsResponse instance) => + { + 'group': instance.group, + }; + +RunDartTestRequest _$RunDartTestRequestFromJson(Map json) => + RunDartTestRequest( + name: json['name'] as String, + ); + +Map _$RunDartTestRequestToJson(RunDartTestRequest instance) => + { + 'name': instance.name, + }; + +RunDartTestResponse _$RunDartTestResponseFromJson(Map json) => + RunDartTestResponse( + result: $enumDecode(_$RunDartTestResponseResultEnumMap, json['result']), + details: json['details'] as String?, + ); + +Map _$RunDartTestResponseToJson( + RunDartTestResponse instance) => + { + 'result': _$RunDartTestResponseResultEnumMap[instance.result]!, + 'details': instance.details, + }; + +const _$RunDartTestResponseResultEnumMap = { + RunDartTestResponseResult.success: 'success', + RunDartTestResponseResult.skipped: 'skipped', + RunDartTestResponseResult.failure: 'failure', +}; + +ConfigureRequest _$ConfigureRequestFromJson(Map json) => + ConfigureRequest( + findTimeoutMillis: json['findTimeoutMillis'] as int, + ); + +Map _$ConfigureRequestToJson(ConfigureRequest instance) => + { + 'findTimeoutMillis': instance.findTimeoutMillis, + }; + +OpenAppRequest _$OpenAppRequestFromJson(Map json) => + OpenAppRequest( + appId: json['appId'] as String, + ); + +Map _$OpenAppRequestToJson(OpenAppRequest instance) => + { + 'appId': instance.appId, + }; + +OpenQuickSettingsRequest _$OpenQuickSettingsRequestFromJson( + Map json) => + OpenQuickSettingsRequest(); + +Map _$OpenQuickSettingsRequestToJson( + OpenQuickSettingsRequest instance) => + {}; + +Selector _$SelectorFromJson(Map json) => Selector( + text: json['text'] as String?, + textStartsWith: json['textStartsWith'] as String?, + textContains: json['textContains'] as String?, + className: json['className'] as String?, + contentDescription: json['contentDescription'] as String?, + contentDescriptionStartsWith: + json['contentDescriptionStartsWith'] as String?, + contentDescriptionContains: json['contentDescriptionContains'] as String?, + resourceId: json['resourceId'] as String?, + instance: json['instance'] as int?, + enabled: json['enabled'] as bool?, + focused: json['focused'] as bool?, + pkg: json['pkg'] as String?, + ); + +Map _$SelectorToJson(Selector instance) => { + 'text': instance.text, + 'textStartsWith': instance.textStartsWith, + 'textContains': instance.textContains, + 'className': instance.className, + 'contentDescription': instance.contentDescription, + 'contentDescriptionStartsWith': instance.contentDescriptionStartsWith, + 'contentDescriptionContains': instance.contentDescriptionContains, + 'resourceId': instance.resourceId, + 'instance': instance.instance, + 'enabled': instance.enabled, + 'focused': instance.focused, + 'pkg': instance.pkg, + }; + +GetNativeViewsRequest _$GetNativeViewsRequestFromJson( + Map json) => + GetNativeViewsRequest( + selector: Selector.fromJson(json['selector'] as Map), + appId: json['appId'] as String, + ); + +Map _$GetNativeViewsRequestToJson( + GetNativeViewsRequest instance) => + { + 'selector': instance.selector, + 'appId': instance.appId, + }; + +GetNativeUITreeRespone _$GetNativeUITreeResponeFromJson( + Map json) => + GetNativeUITreeRespone( + roots: (json['roots'] as List) + .map((e) => NativeView.fromJson(e as Map)) + .toList(), + ); + +Map _$GetNativeUITreeResponeToJson( + GetNativeUITreeRespone instance) => + { + 'roots': instance.roots, + }; + +NativeView _$NativeViewFromJson(Map json) => NativeView( + className: json['className'] as String?, + text: json['text'] as String?, + contentDescription: json['contentDescription'] as String?, + focused: json['focused'] as bool, + enabled: json['enabled'] as bool, + childCount: json['childCount'] as int?, + resourceName: json['resourceName'] as String?, + applicationPackage: json['applicationPackage'] as String?, + children: (json['children'] as List) + .map((e) => NativeView.fromJson(e as Map)) + .toList(), + ); + +Map _$NativeViewToJson(NativeView instance) => + { + 'className': instance.className, + 'text': instance.text, + 'contentDescription': instance.contentDescription, + 'focused': instance.focused, + 'enabled': instance.enabled, + 'childCount': instance.childCount, + 'resourceName': instance.resourceName, + 'applicationPackage': instance.applicationPackage, + 'children': instance.children, + }; + +GetNativeViewsResponse _$GetNativeViewsResponseFromJson( + Map json) => + GetNativeViewsResponse( + nativeViews: (json['nativeViews'] as List) + .map((e) => NativeView.fromJson(e as Map)) + .toList(), + ); + +Map _$GetNativeViewsResponseToJson( + GetNativeViewsResponse instance) => + { + 'nativeViews': instance.nativeViews, + }; + +TapRequest _$TapRequestFromJson(Map json) => TapRequest( + selector: Selector.fromJson(json['selector'] as Map), + appId: json['appId'] as String, + ); + +Map _$TapRequestToJson(TapRequest instance) => + { + 'selector': instance.selector, + 'appId': instance.appId, + }; + +EnterTextRequest _$EnterTextRequestFromJson(Map json) => + EnterTextRequest( + data: json['data'] as String, + appId: json['appId'] as String, + index: json['index'] as int?, + selector: json['selector'] == null + ? null + : Selector.fromJson(json['selector'] as Map), + keyboardBehavior: + $enumDecode(_$KeyboardBehaviorEnumMap, json['keyboardBehavior']), + ); + +Map _$EnterTextRequestToJson(EnterTextRequest instance) => + { + 'data': instance.data, + 'appId': instance.appId, + 'index': instance.index, + 'selector': instance.selector, + 'keyboardBehavior': _$KeyboardBehaviorEnumMap[instance.keyboardBehavior]!, + }; + +const _$KeyboardBehaviorEnumMap = { + KeyboardBehavior.showAndDismiss: 'showAndDismiss', + KeyboardBehavior.alternative: 'alternative', +}; + +SwipeRequest _$SwipeRequestFromJson(Map json) => SwipeRequest( + startX: (json['startX'] as num).toDouble(), + startY: (json['startY'] as num).toDouble(), + endX: (json['endX'] as num).toDouble(), + endY: (json['endY'] as num).toDouble(), + steps: json['steps'] as int, + ); + +Map _$SwipeRequestToJson(SwipeRequest instance) => + { + 'startX': instance.startX, + 'startY': instance.startY, + 'endX': instance.endX, + 'endY': instance.endY, + 'steps': instance.steps, + }; + +WaitUntilVisibleRequest _$WaitUntilVisibleRequestFromJson( + Map json) => + WaitUntilVisibleRequest( + selector: Selector.fromJson(json['selector'] as Map), + appId: json['appId'] as String, + ); + +Map _$WaitUntilVisibleRequestToJson( + WaitUntilVisibleRequest instance) => + { + 'selector': instance.selector, + 'appId': instance.appId, + }; + +DarkModeRequest _$DarkModeRequestFromJson(Map json) => + DarkModeRequest( + appId: json['appId'] as String, + ); + +Map _$DarkModeRequestToJson(DarkModeRequest instance) => + { + 'appId': instance.appId, + }; + +Notification _$NotificationFromJson(Map json) => Notification( + appName: json['appName'] as String?, + title: json['title'] as String, + content: json['content'] as String, + raw: json['raw'] as String?, + ); + +Map _$NotificationToJson(Notification instance) => + { + 'appName': instance.appName, + 'title': instance.title, + 'content': instance.content, + 'raw': instance.raw, + }; + +GetNotificationsResponse _$GetNotificationsResponseFromJson( + Map json) => + GetNotificationsResponse( + notifications: (json['notifications'] as List) + .map((e) => Notification.fromJson(e as Map)) + .toList(), + ); + +Map _$GetNotificationsResponseToJson( + GetNotificationsResponse instance) => + { + 'notifications': instance.notifications, + }; + +GetNotificationsRequest _$GetNotificationsRequestFromJson( + Map json) => + GetNotificationsRequest(); + +Map _$GetNotificationsRequestToJson( + GetNotificationsRequest instance) => + {}; + +TapOnNotificationRequest _$TapOnNotificationRequestFromJson( + Map json) => + TapOnNotificationRequest( + index: json['index'] as int?, + selector: json['selector'] == null + ? null + : Selector.fromJson(json['selector'] as Map), + ); + +Map _$TapOnNotificationRequestToJson( + TapOnNotificationRequest instance) => + { + 'index': instance.index, + 'selector': instance.selector, + }; + +PermissionDialogVisibleResponse _$PermissionDialogVisibleResponseFromJson( + Map json) => + PermissionDialogVisibleResponse( + visible: json['visible'] as bool, + ); + +Map _$PermissionDialogVisibleResponseToJson( + PermissionDialogVisibleResponse instance) => + { + 'visible': instance.visible, + }; + +PermissionDialogVisibleRequest _$PermissionDialogVisibleRequestFromJson( + Map json) => + PermissionDialogVisibleRequest( + timeoutMillis: json['timeoutMillis'] as int, + ); + +Map _$PermissionDialogVisibleRequestToJson( + PermissionDialogVisibleRequest instance) => + { + 'timeoutMillis': instance.timeoutMillis, + }; + +HandlePermissionRequest _$HandlePermissionRequestFromJson( + Map json) => + HandlePermissionRequest( + code: $enumDecode(_$HandlePermissionRequestCodeEnumMap, json['code']), + ); + +Map _$HandlePermissionRequestToJson( + HandlePermissionRequest instance) => + { + 'code': _$HandlePermissionRequestCodeEnumMap[instance.code]!, + }; + +const _$HandlePermissionRequestCodeEnumMap = { + HandlePermissionRequestCode.whileUsing: 'whileUsing', + HandlePermissionRequestCode.onlyThisTime: 'onlyThisTime', + HandlePermissionRequestCode.denied: 'denied', +}; + +SetLocationAccuracyRequest _$SetLocationAccuracyRequestFromJson( + Map json) => + SetLocationAccuracyRequest( + locationAccuracy: $enumDecode( + _$SetLocationAccuracyRequestLocationAccuracyEnumMap, + json['locationAccuracy']), + ); + +Map _$SetLocationAccuracyRequestToJson( + SetLocationAccuracyRequest instance) => + { + 'locationAccuracy': _$SetLocationAccuracyRequestLocationAccuracyEnumMap[ + instance.locationAccuracy]!, + }; + +const _$SetLocationAccuracyRequestLocationAccuracyEnumMap = { + SetLocationAccuracyRequestLocationAccuracy.coarse: 'coarse', + SetLocationAccuracyRequestLocationAccuracy.fine: 'fine', +}; diff --git a/packages/patrol_devtools_extension/lib/api/patrol_service_extension_api.dart b/packages/patrol_devtools_extension/lib/api/patrol_service_extension_api.dart new file mode 100644 index 000000000..e4c9de799 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/api/patrol_service_extension_api.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:patrol_devtools_extension/api/contracts.dart'; +import 'package:vm_service/vm_service.dart'; + +sealed class ApiResult extends Equatable { + const ApiResult(); + + bool get isSuccess => this is ApiSuccess; + + bool get isFailure => this is ApiFailure; +} + +final class ApiSuccess extends ApiResult { + const ApiSuccess(this.data); + + final T data; + + @override + List get props => [data]; +} + +final class ApiFailure extends ApiResult { + const ApiFailure(this.error, this.stackTrace); + + final Object error; + final StackTrace? stackTrace; + + @override + List get props => [error, stackTrace]; +} + +class PatrolServiceExtensionApi { + const PatrolServiceExtensionApi({ + required VmService service, + required ValueListenable isolate, + }) : _isolate = isolate, + _service = service; + + final VmService _service; + final ValueListenable _isolate; + + Future> getNativeUITree() { + return _callServiceExtension( + 'patrol.getNativeUITree', + {}, + (dynamic json) => + GetNativeUITreeRespone.fromJson(json as Map), + ); + } + + Future> _callServiceExtension( + String methodName, + Map args, + TResult Function(dynamic json) resultFactory, + ) async { + try { + final extensionName = 'ext.flutter.$methodName'; + final r = await _service.callServiceExtension( + extensionName, + isolateId: _isolate.value!.id, //Verify + args: args, + ); + + final json = r.json!; + if (json['success'] != true) { + return ApiFailure(json['success'] as String, null); + } + + final res = resultFactory(json['result']); + + return ApiSuccess(res); + } catch (e, t) { + return ApiFailure(e, t); + } + } +} diff --git a/packages/patrol_devtools_extension/lib/main.dart b/packages/patrol_devtools_extension/lib/main.dart new file mode 100644 index 000000000..9fd976384 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/main.dart @@ -0,0 +1,18 @@ +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/widgets.dart'; +import 'package:patrol_devtools_extension/patrol_devtools_extension.dart'; + +void main() { + runApp(const PatrolPackageDevToolsExtension()); +} + +class PatrolPackageDevToolsExtension extends StatelessWidget { + const PatrolPackageDevToolsExtension({super.key}); + + @override + Widget build(BuildContext context) { + return const DevToolsExtension( + child: PatrolDevToolsExtension(), + ); + } +} diff --git a/packages/patrol_devtools_extension/lib/native_inspector/native_inspector.dart b/packages/patrol_devtools_extension/lib/native_inspector/native_inspector.dart new file mode 100644 index 000000000..31ea87148 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/native_inspector.dart @@ -0,0 +1,54 @@ +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:patrol_devtools_extension/native_inspector/native_view_details.dart'; +import 'package:patrol_devtools_extension/native_inspector/native_view_hierarchy.dart'; +import 'package:patrol_devtools_extension/native_inspector/node.dart'; + +class NativeInspector extends HookWidget { + const NativeInspector({ + super.key, + required this.onNodeChanged, + required this.onRefreshPressed, + required this.roots, + required this.currentNode, + }); + + final List roots; + final Node? currentNode; + final ValueChanged onNodeChanged; + final VoidCallback onRefreshPressed; + + @override + Widget build(BuildContext context) { + final fullNodeNames = useState(false); + + final splitAxis = Split.axisFor(context, 0.85); + final child = Split( + axis: splitAxis, + initialFractions: const [0.6, 0.4], + children: [ + RoundedOutlinedBorder( + clip: true, + child: NativeViewHierarchy( + fullNodeNames: fullNodeNames, + onRefreshPressed: onRefreshPressed, + roots: roots, + props: NodeProps( + currentNode: currentNode, + onNodeTap: onNodeChanged, + fullNodeName: fullNodeNames.value, + colorScheme: Theme.of(context).colorScheme, + ), + ), + ), + RoundedOutlinedBorder( + clip: true, + child: NativeViewDetails(currentNode: currentNode), + ), + ], + ); + + return child; + } +} diff --git a/packages/patrol_devtools_extension/lib/native_inspector/native_view_details.dart b/packages/patrol_devtools_extension/lib/native_inspector/native_view_details.dart new file mode 100644 index 000000000..4ced99338 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/native_view_details.dart @@ -0,0 +1,250 @@ +import 'package:collection/collection.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:patrol_devtools_extension/native_inspector/node.dart'; +import 'package:patrol_devtools_extension/native_inspector/widgets/overflowing_flex.dart'; + +class NativeViewDetails extends StatelessWidget { + const NativeViewDetails({super.key, required this.currentNode}); + + final Node? currentNode; + + @override + Widget build(BuildContext context) { + return ScaffoldMessenger( + child: Scaffold( + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const _HeaderDecoration( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: denseSpacing), + child: SizedBox( + width: double.infinity, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'Native view details', + maxLines: 1, + ), + ), + ), + ), + ), + Expanded( + child: currentNode != null + ? Padding( + padding: + const EdgeInsets.symmetric(horizontal: denseSpacing), + child: _NodeDetails(node: currentNode!), + ) + : Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(densePadding), + child: const Text( + 'Select a node to view its details', + textAlign: TextAlign.center, + maxLines: 4, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _HeaderDecoration extends StatelessWidget { + const _HeaderDecoration({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + height: defaultHeaderHeight(isDense: _isDense()), + decoration: BoxDecoration( + border: Border( + bottom: defaultBorderSide(Theme.of(context)), + ), + ), + child: child, + ); + } + + bool _isDense() { + return ideTheme.embed; + } +} + +class _NodeDetails extends HookWidget { + const _NodeDetails({required this.node}); + + final Node node; + + void _onCopyClick(BuildContext context, _KeyValueItem kvItem) { + Clipboard.setData(ClipboardData(text: kvItem.copyValue)); + + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied ${kvItem.copyValue}', maxLines: 1), + ), + ); + } + + @override + Widget build(BuildContext context) { + final hoveredIndex = useState(null); + + final view = node.nativeView; + final rows = [ + _KeyValueItem('pkg:', view.applicationPackage), + _KeyValueItem('childCount:', view.childCount), + _KeyValueItem('className:', view.className), + _KeyValueItem('contentDescription:', view.contentDescription), + _KeyValueItem('enabled:', view.enabled), + _KeyValueItem('focused:', view.focused), + _KeyValueItem('resourceId:', view.resourceName), + _KeyValueItem('text:', view.text), + ]; + + final unimportantTextStyle = TextStyle( + color: Theme.of(context).colorScheme.isLight + ? Colors.grey.shade500 + : Colors.grey.shade600, + ); + + return SelectionArea( + child: OverflowingFlex( + direction: Axis.horizontal, + children: [ + IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...rows.mapIndexed( + (index, kvItem) => Builder( + builder: (context) { + final hovered = hoveredIndex.value == index; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _onCopyClick(context, kvItem), + child: MouseRegion( + onEnter: (_) => hoveredIndex.value = index, + onExit: (_) => hoveredIndex.value = null, + child: Container( + height: 32, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 8), + decoration: BoxDecoration( + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(8), + ), + color: hovered + ? Theme.of(context) + .colorScheme + .primaryContainer + : null, + ), + child: Text( + kvItem.key, + style: kvItem.important + ? null + : unimportantTextStyle, + maxLines: 1, + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...rows.mapIndexed( + (index, kvItem) => Builder( + builder: (context) { + final hovered = hoveredIndex.value == index; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _onCopyClick(context, kvItem), + child: Container( + height: 32, + padding: const EdgeInsets.only(left: 8), + decoration: BoxDecoration( + borderRadius: const BorderRadius.horizontal( + right: Radius.circular(8), + ), + color: hovered + ? Theme.of(context).colorScheme.primaryContainer + : null, + ), + child: MouseRegion( + onEnter: (_) => hoveredIndex.value = index, + onExit: (_) => hoveredIndex.value = null, + child: Stack( + alignment: Alignment.center, + children: [ + Row( + children: [ + Text( + kvItem.value, + maxLines: 1, + style: kvItem.important + ? null + : unimportantTextStyle, + ), + SizedBox(width: defaultIconSize * 2), + ], + ), + Align( + alignment: Alignment.centerRight, + child: Opacity( + opacity: hovered ? 1 : 0, + child: const Icon(Icons.copy), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _KeyValueItem { + _KeyValueItem(this.key, Object? val) : important = val != null { + value = switch (val) { + null => 'null', + final String v => '"$v"', + _ => val.toString(), + }; + + copyValue = '$key $value'; + } + + final String key; + late final String value; + late final String copyValue; + final bool important; +} diff --git a/packages/patrol_devtools_extension/lib/native_inspector/native_view_hierarchy.dart b/packages/patrol_devtools_extension/lib/native_inspector/native_view_hierarchy.dart new file mode 100644 index 000000000..6218db86f --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/native_view_hierarchy.dart @@ -0,0 +1,414 @@ +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:patrol_devtools_extension/native_inspector/node.dart'; +import 'package:patrol_devtools_extension/native_inspector/widgets/header_decoration.dart'; +import 'package:patrol_devtools_extension/native_inspector/widgets/overflowing_flex.dart'; + +class NodeProps { + NodeProps({ + required this.currentNode, + required this.onNodeTap, + required this.fullNodeName, + required this.colorScheme, + }); + + final Node? currentNode; + final ValueChanged onNodeTap; + final bool fullNodeName; + final ColorScheme colorScheme; +} + +class NativeViewHierarchy extends StatelessWidget { + const NativeViewHierarchy({ + super.key, + required this.roots, + required this.props, + required this.onRefreshPressed, + required this.fullNodeNames, + }); + + final List roots; + final NodeProps props; + final VoidCallback onRefreshPressed; + final ValueNotifier fullNodeNames; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _InspectorTreeControls( + onRefreshPressed: onRefreshPressed, + fullNodeNames: fullNodeNames, + ), + Expanded( + child: roots.isEmpty + ? Center( + child: DevToolsButton( + label: 'Fetch native view hierarchy', + onPressed: onRefreshPressed, + ), + ) + : _NativeViewHierarchyTree( + roots: roots, + props: props, + ), + ), + ], + ); + } +} + +class _NativeViewHierarchyTree extends StatefulWidget { + const _NativeViewHierarchyTree({ + required this.roots, + required this.props, + }); + + final List roots; + final NodeProps props; + + @override + State<_NativeViewHierarchyTree> createState() => + _NativeViewHierarchyTreeState(); +} + +class _NativeViewHierarchyTreeState extends State<_NativeViewHierarchyTree> { + late final ScrollController horizontalScrollController; + late final ScrollController verticalScrollController; + + @override + void initState() { + super.initState(); + + horizontalScrollController = ScrollController(); + verticalScrollController = ScrollController(); + } + + @override + void dispose() { + horizontalScrollController.dispose(); + verticalScrollController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return GestureDetector( + onTap: () => widget.props.onNodeTap(null), + child: Scrollbar( + controller: horizontalScrollController, + thumbVisibility: true, + thickness: 12, + child: SingleChildScrollView( + controller: horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: 1000, + // This is ugly, but we want scrollbar on the left side. + child: Directionality( + textDirection: TextDirection.rtl, + child: Scrollbar( + controller: verticalScrollController, + thickness: 12, + thumbVisibility: true, + child: Directionality( + textDirection: TextDirection.ltr, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ), + child: ListView( + controller: verticalScrollController, + children: [ + for (final root in widget.roots) + _Node(node: root, props: widget.props), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +class _InspectorTreeControls extends StatelessWidget { + const _InspectorTreeControls({ + required this.onRefreshPressed, + required this.fullNodeNames, + }); + + final VoidCallback onRefreshPressed; + final ValueNotifier fullNodeNames; + + @override + Widget build(BuildContext context) { + return HeaderDecoration( + child: OverflowingFlex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const OverflowingFlex( + direction: Axis.horizontal, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: denseSpacing), + child: Text('Native view tree', maxLines: 1), + ), + ], + ), + OverflowingFlex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ControlButton( + message: 'Full node names', + onPressed: () { + fullNodeNames.value = !fullNodeNames.value; + }, + icon: fullNodeNames.value + ? Icons.visibility + : Icons.visibility_off, + ), + _ControlButton( + icon: Icons.refresh, + message: 'Refresh tree', + onPressed: onRefreshPressed, + ), + ], + ), + ], + ), + ); + } +} + +class _ControlButton extends StatelessWidget { + const _ControlButton({ + required this.message, + required this.onPressed, + required this.icon, + }); + + final String message; + final VoidCallback onPressed; + final IconData icon; + + @override + Widget build(BuildContext context) { + return DevToolsTooltip( + message: message, + child: TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: onPressed, + child: Icon( + icon, + size: actionsIconSize, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } +} + +class _Node extends HookWidget { + const _Node({ + required this.node, + required this.props, + }); + + final NodeProps props; + final Node node; + + @override + Widget build(BuildContext context) { + final iconSize = defaultIconSize; + final nodeNeedsLines = (node.parent?.children.length ?? 0) > 1; + + final isExpanded = useState(true); + + final isSelected = props.currentNode == node; + final isHovered = useState(false); + + final backgroundColor = switch (isHovered.value) { + true => props.colorScheme.secondaryContainer, + false => isSelected ? props.colorScheme.primaryContainer : null, + }; + + final child = Container( + padding: EdgeInsets.only(left: iconSize), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + OverflowingFlex( + direction: Axis.horizontal, + children: [ + MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + // color: + // isSelected ? props.colorScheme.primaryContainer : null, + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(8), + ), + child: OverflowingFlex( + direction: Axis.horizontal, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + if (node.children.isNotEmpty) + InkWell( + onTap: () => isExpanded.value = !isExpanded.value, + child: AnimatedRotation( + turns: isExpanded.value ? 1 : 6 / 8, + duration: const Duration(milliseconds: 150), + child: Icon( + Icons.expand_more, + size: iconSize, + ), + ), + ) + else + SizedBox( + width: iconSize, + height: iconSize, + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => props.onNodeTap(node), + child: OverflowingFlex( + direction: Axis.horizontal, + children: [ + Container( + decoration: const BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + ), + width: iconSize, + height: iconSize, + child: Center( + child: Text( + node.initialCharacter, + style: DefaultTextStyle.of(context) + .style + .copyWith( + fontSize: iconSize * 0.7, + color: props.colorScheme.background, + ), + ), + ), + ), + const SizedBox(width: 4), + Text( + props.fullNodeName + ? node.fullNodeName + : node.shortNodeName, + overflow: TextOverflow.ellipsis, + style: + DefaultTextStyle.of(context).style.copyWith( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + const SizedBox(width: 8), + ], + ), + ), + ], + ), + ), + ), + ], + ), + if (isExpanded.value) + Column( + children: node.children + .map( + (e) => _Node( + props: props, + node: e, + ), + ) + .toList(), + ), + ], + ), + ); + + return nodeNeedsLines + ? CustomPaint( + painter: _LinesPainter( + colorScheme: props.colorScheme, + iconSize: iconSize, + lastChildren: node == node.parent?.children.last, + hasExpandMoreIcon: node.children.isNotEmpty, + ), + child: child, + ) + : child; + } +} + +class _LinesPainter extends CustomPainter { + _LinesPainter({ + required this.iconSize, + required this.lastChildren, + required this.colorScheme, + required this.hasExpandMoreIcon, + }); + + final ColorScheme colorScheme; + final double iconSize; + final bool hasExpandMoreIcon; + final bool lastChildren; + + @override + void paint(Canvas canvas, Size size) { + final paint = _defaultLinePaint(colorScheme); + final halfOfIconSize = iconSize / 2; + + final yEnd = lastChildren ? halfOfIconSize : size.height; + + canvas + ..drawLine( + Offset(halfOfIconSize, 0), + Offset(halfOfIconSize, yEnd), + paint, + ) + ..drawLine( + Offset(halfOfIconSize, halfOfIconSize), + Offset( + hasExpandMoreIcon ? iconSize : (iconSize + halfOfIconSize), + halfOfIconSize, + ), + paint, + ); + } + + @override + bool shouldRepaint(_LinesPainter oldDelegate) => + oldDelegate.colorScheme.isLight != colorScheme.isLight; +} + +Paint _defaultLinePaint(ColorScheme colorScheme) => Paint() + ..color = colorScheme.isLight + ? Colors.black54 + : const Color.fromARGB(255, 200, 200, 200) + ..strokeWidth = 1.0; diff --git a/packages/patrol_devtools_extension/lib/native_inspector/node.dart b/packages/patrol_devtools_extension/lib/native_inspector/node.dart new file mode 100644 index 000000000..a59dc0d78 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/node.dart @@ -0,0 +1,52 @@ +import 'package:patrol_devtools_extension/api/contracts.dart'; + +class Node { + Node(this.nativeView, this.parent, {required this.androidNode}) { + children = nativeView.children + .map((e) => Node(e, this, androidNode: androidNode)) + .toList(); + + fullNodeName = _nodeName(nativeView.className, nativeView.resourceName); + + shortNodeName = _shortNodeName( + nativeView.className, + nativeView.resourceName, + ); + + initialCharacter = + shortNodeName.isNotEmpty ? shortNodeName[0].toUpperCase() : ''; + } + + final NativeView nativeView; + final Node? parent; + final bool androidNode; + + late final List children; + late final String fullNodeName; + late final String shortNodeName; + late final String initialCharacter; + + static List ignoreTypePrefixes = ['android.widget.', 'android.view.']; + + String _shortNodeName(String? type, String? resourceName) { + var typeName = type ?? ''; + + if (androidNode && typeName.isNotEmpty) { + for (final prefix in ignoreTypePrefixes) { + if (typeName.startsWith(prefix)) { + typeName = typeName.substring(prefix.length); + break; + } + } + } + + return _nodeName(typeName, resourceName); + } + + String _nodeName(String? type, String? resourceName) { + if (resourceName == null || resourceName.isEmpty) { + return '$type'; + } + return "$type-[<'$resourceName'>]"; + } +} diff --git a/packages/patrol_devtools_extension/lib/native_inspector/widgets/header_decoration.dart b/packages/patrol_devtools_extension/lib/native_inspector/widgets/header_decoration.dart new file mode 100644 index 000000000..78fa63a07 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/widgets/header_decoration.dart @@ -0,0 +1,25 @@ +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +class HeaderDecoration extends StatelessWidget { + const HeaderDecoration({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + height: defaultHeaderHeight(isDense: _isDense()), + decoration: BoxDecoration( + border: Border( + bottom: defaultBorderSide(Theme.of(context)), + ), + ), + child: child, + ); + } + + bool _isDense() { + return ideTheme.embed; + } +} diff --git a/packages/patrol_devtools_extension/lib/native_inspector/widgets/overflowing_flex.dart b/packages/patrol_devtools_extension/lib/native_inspector/widgets/overflowing_flex.dart new file mode 100644 index 000000000..08a8dbaf8 --- /dev/null +++ b/packages/patrol_devtools_extension/lib/native_inspector/widgets/overflowing_flex.dart @@ -0,0 +1,85 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// A [Flex] that doesn't get angry when contents overflow. +/// +/// Author: Albert Wolszon +/// Source: https://gist.github.com/Albert221/b7320c9109e7b22dcc31cfda05bcac26 +class OverflowingFlex extends Flex { + const OverflowingFlex({ + super.key, + required super.direction, + required super.children, + this.allowOverflow = true, + super.mainAxisSize, + super.mainAxisAlignment, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.clipBehavior, + }); + + final bool allowOverflow; + + @override + _RenderOverflowingFlex createRenderObject(BuildContext context) { + return _RenderOverflowingFlex( + direction: direction, + allowOverflow: allowOverflow, + mainAxisAlignment: mainAxisAlignment, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, + clipBehavior: clipBehavior, + ); + } +} + +class _RenderOverflowingFlex extends RenderFlex { + _RenderOverflowingFlex({ + super.direction, + bool allowOverflow = true, + super.mainAxisSize, + super.mainAxisAlignment, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.clipBehavior, + }) : _allowOverflow = allowOverflow; + + bool _allowOverflow; + + bool get allowOverflow => _allowOverflow; + + set allowOverflow(bool value) { + if (value != _allowOverflow) { + _allowOverflow = value; + markNeedsPaint(); + } + } + + @override + void paintOverflowIndicator( + PaintingContext context, + Offset offset, + Rect containerRect, + Rect childRect, { + List? overflowHints, + }) { + if (_allowOverflow) { + // Don't log or show overflow indicator. Everything's good ova here. + } else { + super.paintOverflowIndicator( + context, + offset, + containerRect, + childRect, + overflowHints: overflowHints, + ); + } + } +} diff --git a/packages/patrol_devtools_extension/lib/patrol_devtools_extension.dart b/packages/patrol_devtools_extension/lib/patrol_devtools_extension.dart new file mode 100644 index 000000000..df236275b --- /dev/null +++ b/packages/patrol_devtools_extension/lib/patrol_devtools_extension.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:patrol_devtools_extension/api/patrol_service_extension_api.dart'; +import 'package:patrol_devtools_extension/native_inspector/native_inspector.dart'; +import 'package:patrol_devtools_extension/native_inspector/node.dart'; + +class PatrolDevToolsExtension extends StatefulWidget { + const PatrolDevToolsExtension({super.key}); + + @override + State createState() { + return _PatrolDevToolsExtensionState(); + } +} + +class _PatrolDevToolsExtensionState extends State { + final runner = _Runner(); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: runner, + builder: (context, state, child) { + return NativeInspector( + roots: state.roots, + currentNode: state.currentNode, + onNodeChanged: runner.changeNode, + onRefreshPressed: runner.getNativeUITree, + ); + }, + ); + } +} + +class _Runner extends ValueNotifier<_State> { + _Runner() : super(_State()); + + bool get isAndroidApp { + return serviceManager.connectedApp?.operatingSystem == 'android'; + } + + void changeNode(Node? node) { + value.currentNode = node; + notifyListeners(); + } + + Future getNativeUITree() async { + value + ..roots = [] + ..currentNode = null; + + final api = PatrolServiceExtensionApi( + service: serviceManager.service!, + isolate: serviceManager.isolateManager.mainIsolate, + ); + + final result = await api.getNativeUITree(); + + switch (result) { + case ApiSuccess(:final data): + value.roots = data.roots + .map((e) => Node(e, null, androidNode: isAndroidApp)) + .toList(); + case ApiFailure _: + // TODO: Handle failure + } + + notifyListeners(); + } +} + +class _State { + List roots = []; + Node? currentNode; +} diff --git a/packages/patrol_devtools_extension/publish_to_patrol_extension b/packages/patrol_devtools_extension/publish_to_patrol_extension new file mode 100755 index 000000000..88a8e4034 --- /dev/null +++ b/packages/patrol_devtools_extension/publish_to_patrol_extension @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" || exit + +out="../patrol/extension/devtools" + +mkdir -p "$out/build" + +flutter pub get +dart run devtools_extensions build_and_copy --source="." --dest="$out" diff --git a/packages/patrol_devtools_extension/pubspec.lock b/packages/patrol_devtools_extension/pubspec.lock new file mode 100644 index 000000000..a8b6d0211 --- /dev/null +++ b/packages/patrol_devtools_extension/pubspec.lock @@ -0,0 +1,557 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + custom_lint: + dependency: transitive + description: + name: custom_lint + sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + devtools_app_shared: + dependency: "direct main" + description: + name: devtools_app_shared + sha256: "2ba4cbc1e863d25ddc89ac367c3361a806be613bc2cc90d978f884029f179052" + url: "https://pub.dev" + source: hosted + version: "0.0.4" + devtools_extensions: + dependency: "direct main" + description: + name: devtools_extensions + sha256: db51af213b7b5413ad8fcf37bdccc6defbb6cce109e536a9b3d8261062f71956 + url: "https://pub.dev" + source: hosted + version: "0.0.6" + devtools_shared: + dependency: transitive + description: + name: devtools_shared + sha256: "81528e760c89f0e9e093e8a01135bb067e4ea7beab509e8effc2916452c4a4c1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + extension_discovery: + dependency: transitive + description: + name: extension_discovery + sha256: "20735622d0763865f9d94c3ecdce4441174530870760253e9d364fb4f3da8688" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + leancode_lint: + dependency: "direct dev" + description: + name: leancode_lint + sha256: "1e99cba16e084a18ce966a7df270d6da6a11ab236caac716aa1fb2359eb277eb" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: acfcd63c00ec3d5a7894b0e2a875893716d31958fe03f064734dba7dfd9113d9 + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sse: + dependency: transitive + description: + name: sse + sha256: "3ff9088cac3f45aa8b91336f1962e3ea6c81baaba0bbba361c05f8aa7fb59442" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + usage: + dependency: transitive + description: + name: usage + sha256: "0bdbde65a6e710343d02a56552eeaefd20b735e04bfb6b3ee025b6b22e8d0e15" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: "direct main" + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + url: "https://pub.dev" + source: hosted + version: "2.1.1" +sdks: + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.16.0-0" diff --git a/packages/patrol_devtools_extension/pubspec.yaml b/packages/patrol_devtools_extension/pubspec.yaml new file mode 100644 index 000000000..27c5ac810 --- /dev/null +++ b/packages/patrol_devtools_extension/pubspec.yaml @@ -0,0 +1,29 @@ +name: patrol_devtools_extension +description: A new Flutter project. +publish_to: none + +version: 0.1.0 + +environment: + sdk: '>=3.2.0-0 <4.0.0' + flutter: 3.16.0-0 + +dependencies: + collection: ^1.18.0 + cupertino_icons: ^1.0.6 + devtools_app_shared: ^0.0.4 + devtools_extensions: ^0.0.6 + equatable: ^2.0.5 + flutter: + sdk: flutter + flutter_hooks: ^0.20.3 + json_annotation: ^4.8.1 + vm_service: ^11.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + leancode_lint: ^7.0.0+1 + +flutter: + uses-material-design: true diff --git a/packages/patrol_devtools_extension/test/example_test.dart b/packages/patrol_devtools_extension/test/example_test.dart new file mode 100644 index 000000000..c6cbc8798 --- /dev/null +++ b/packages/patrol_devtools_extension/test/example_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('empty test so CI passes', () { + expect(2 + 2, equals(4)); + }); +} diff --git a/packages/patrol_devtools_extension/web/favicon.png b/packages/patrol_devtools_extension/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/packages/patrol_devtools_extension/web/favicon.png differ diff --git a/packages/patrol_devtools_extension/web/icons/Icon-192.png b/packages/patrol_devtools_extension/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/packages/patrol_devtools_extension/web/icons/Icon-192.png differ diff --git a/packages/patrol_devtools_extension/web/icons/Icon-512.png b/packages/patrol_devtools_extension/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/packages/patrol_devtools_extension/web/icons/Icon-512.png differ diff --git a/packages/patrol_devtools_extension/web/icons/Icon-maskable-192.png b/packages/patrol_devtools_extension/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/packages/patrol_devtools_extension/web/icons/Icon-maskable-192.png differ diff --git a/packages/patrol_devtools_extension/web/icons/Icon-maskable-512.png b/packages/patrol_devtools_extension/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/packages/patrol_devtools_extension/web/icons/Icon-maskable-512.png differ diff --git a/packages/patrol_devtools_extension/web/index.html b/packages/patrol_devtools_extension/web/index.html new file mode 100644 index 000000000..66e48c6af --- /dev/null +++ b/packages/patrol_devtools_extension/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + patrol_devtools_extension + + + + + + + + + + diff --git a/packages/patrol_devtools_extension/web/manifest.json b/packages/patrol_devtools_extension/web/manifest.json new file mode 100644 index 000000000..2c35f57ef --- /dev/null +++ b/packages/patrol_devtools_extension/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "patrol_devtools_extension", + "short_name": "patrol_devtools_extension", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/patrol_finders/CHANGELOG.md b/packages/patrol_finders/CHANGELOG.md index e255be84c..d89ff5c65 100644 --- a/packages/patrol_finders/CHANGELOG.md +++ b/packages/patrol_finders/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +- Bump minimum supported Flutter version to 3.16 +- **BREAKING**: + - Remove deprecated `andSettle` from all `PatrolTester` and `PatrolFinder` + methods. Use `settlePolicy` instead (#1892) + ## 1.0.0 - Initial release as a standalone package (#1606) diff --git a/packages/patrol_finders/analysis_options.yaml b/packages/patrol_finders/analysis_options.yaml index 503b81993..31551b63c 100644 --- a/packages/patrol_finders/analysis_options.yaml +++ b/packages/patrol_finders/analysis_options.yaml @@ -1,5 +1 @@ include: package:leancode_lint/analysis_options_package.yaml - -analyzer: - errors: - deprecated_member_use: ignore # TODO: Remove after deprections are fixed diff --git a/packages/patrol_finders/example/pubspec.yaml b/packages/patrol_finders/example/pubspec.yaml index 24d6aff91..1f3b4c205 100644 --- a/packages/patrol_finders/example/pubspec.yaml +++ b/packages/patrol_finders/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: none version: 1.0.0+1 environment: - sdk: '>=2.18.0 <3.0.0' - flutter: '>=3.3.0' + sdk: '>=3.2.0-0 <4.0.0' + flutter: '>=3.16.0-0' dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/patrol_finders/lib/src/custom_finders/patrol_finder.dart b/packages/patrol_finders/lib/src/custom_finders/patrol_finder.dart index e75931cc6..384e14e81 100644 --- a/packages/patrol_finders/lib/src/custom_finders/patrol_finder.dart +++ b/packages/patrol_finders/lib/src/custom_finders/patrol_finder.dart @@ -132,7 +132,7 @@ Finder createFinder(dynamic matching) { /// /// This is decorator around [Finder] that extends it with Patrol features, but /// also preserves Finder's behavior. -class PatrolFinder extends MatchFinder { +class PatrolFinder implements MatchFinder { /// Creates a new [PatrolFinder] with the given [finder] and [tester]. /// /// Usually, you won't use this constructor directly. Instead, you'll use the @@ -192,24 +192,21 @@ class PatrolFinder extends MatchFinder { /// ``` /// /// This method automatically calls [WidgetTester.pumpAndSettle] after - /// tapping. If you want to disable this behavior, set [andSettle] to false. + /// tapping. If you want to disable this behavior, set [settlePolicy] to + /// [SettlePolicy.noSettle]. /// /// See also: /// - [PatrolFinder.waitUntilVisible], which is used to wait for the widget /// to appear /// - [WidgetController.tap] Future tap({ - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, }) async { await tester.tap( this, - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, visibleTimeout: visibleTimeout, settleTimeout: settleTimeout, ); @@ -241,17 +238,13 @@ class PatrolFinder extends MatchFinder { /// to appear /// - [WidgetController.longPress] Future longPress({ - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, }) async { await tester.longPress( this, - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, visibleTimeout: visibleTimeout, settleTimeout: settleTimeout, ); @@ -275,8 +268,8 @@ class PatrolFinder extends MatchFinder { /// ``` /// /// This method automatically calls [WidgetTester.pumpAndSettle] after - /// entering text. If you want to disable this behavior, set [andSettle] to - /// false. + /// entering text. If you want to disable this behavior, set [settlePolicy] to + /// [SettlePolicy.noSettle]. /// /// See also: /// - [PatrolFinder.waitUntilVisible], which is used to wait for the widget @@ -284,7 +277,6 @@ class PatrolFinder extends MatchFinder { /// - [WidgetTester.enterText] Future enterText( String text, { - @Deprecated('Use settlePolicy instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, @@ -292,10 +284,7 @@ class PatrolFinder extends MatchFinder { await tester.enterText( this, text, - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, visibleTimeout: visibleTimeout, settleTimeout: settleTimeout, ); @@ -318,7 +307,6 @@ class PatrolFinder extends MatchFinder { int maxScrolls = defaultScrollMaxIteration, Duration? settleBetweenScrollsTimeout, Duration? dragDuration, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, }) { return tester.scrollUntilVisible( @@ -328,10 +316,7 @@ class PatrolFinder extends MatchFinder { scrollDirection: scrollDirection, maxScrolls: maxScrolls, settleBetweenScrollsTimeout: settleBetweenScrollsTimeout, - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, dragDuration: dragDuration, ); } @@ -444,6 +429,9 @@ class PatrolFinder extends MatchFinder { /// Returns true if this finder finds at least 1 widget. bool get exists => evaluate().isNotEmpty; + @override + String describeMatch(Plurality plurality) => finder.describeMatch(plurality); + /// Returns true if this finder finds at least 1 visible widget. bool get visible { final isVisible = hitTestable().evaluate().isNotEmpty; @@ -458,13 +446,16 @@ class PatrolFinder extends MatchFinder { } @override - Iterable evaluate() => finder.evaluate(); + FinderResult evaluate() => finder.evaluate(); @override - Iterable apply(Iterable candidates) { - return finder.apply(candidates); + Iterable findInCandidates(Iterable candidates) { + return finder.findInCandidates(candidates); } + @override + bool tryEvaluate() => finder.tryEvaluate(); + @override PatrolFinder get first { // TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548) @@ -495,7 +486,7 @@ class PatrolFinder extends MatchFinder { } @override - bool precache() => finder.precache(); + FinderResult get found => finder.found; @override PatrolFinder hitTestable({Alignment at = Alignment.center}) { @@ -506,10 +497,35 @@ class PatrolFinder extends MatchFinder { Iterable get allCandidates => finder.allCandidates; @override + String toString({bool describeSelf = false}) { + return finder.toString(describeSelf: describeSelf); + } + + @override + bool get hasFound => finder.hasFound; + + @override + void reset() => finder.reset(); + + @override + void runCached(VoidCallback run) => finder.runCached(run); + + @override + bool get skipOffstage => finder.skipOffstage; + + @override + Iterable apply(Iterable candidates) { + // ignore: deprecated_member_use + return finder.apply(candidates); + } + + @override + // ignore: deprecated_member_use String get description => finder.description; @override - String toString() => finder.toString(); + // ignore: deprecated_member_use + bool precache() => finder.precache(); } /// Useful methods that make chained finders more readable. @@ -517,16 +533,12 @@ extension ActionCombiner on Future { /// Same as [PatrolFinder.tap], but on a [PatrolFinder] which is not yet /// visible. Future tap({ - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimoeut, }) async { await (await this).tap( - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, visibleTimeout: visibleTimeout, settleTimeout: settleTimoeut, ); @@ -536,17 +548,13 @@ extension ActionCombiner on Future { /// visible. Future enterText( String text, { - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimoeut, }) async { await (await this).enterText( text, - settlePolicy: chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ), + settlePolicy: settlePolicy, visibleTimeout: visibleTimeout, settleTimeout: settleTimoeut, ); diff --git a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart index 9620e6a38..0ba4a5fc1 100644 --- a/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart +++ b/packages/patrol_finders/lib/src/custom_finders/patrol_tester.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol_finders/patrol_finders.dart'; -import 'package:patrol_finders/src/custom_finders/utils.dart'; /// Common configuration for [PatrolTester] and [PatrolFinder]. class PatrolTesterConfig { @@ -13,9 +12,7 @@ class PatrolTesterConfig { this.existsTimeout = const Duration(seconds: 10), this.visibleTimeout = const Duration(seconds: 10), this.settleTimeout = const Duration(seconds: 10), - @Deprecated('Use settlePolicy argument instead') this.andSettle = true, - // TODO: change default to trySettle, see #1369 (https://github.com/leancodepl/patrol/issues/1369) - this.settlePolicy = SettlePolicy.settle, + this.settlePolicy = SettlePolicy.trySettle, this.dragDuration = const Duration(milliseconds: 100), this.settleBetweenScrollsTimeout = const Duration(seconds: 5), }); @@ -42,12 +39,6 @@ class PatrolTesterConfig { /// [settlePolicy]). final Duration settleTimeout; - /// Whether to call [WidgetTester.pumpAndSettle] after actions such as - /// [PatrolFinder.tap] and [PatrolFinder]. If false, only [WidgetTester.pump] - /// is called. - @Deprecated('Use PatrolTester.settlePolicy instead') - final bool andSettle; - /// Defines which pump method should be called after actions such as /// [PatrolTester.tap], [PatrolTester.enterText], and [PatrolFinder.scrollTo]. /// @@ -69,7 +60,6 @@ class PatrolTesterConfig { Duration? existsTimeout, Duration? visibleTimeout, Duration? settleTimeout, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? dragDuration, }) { @@ -77,9 +67,6 @@ class PatrolTesterConfig { existsTimeout: existsTimeout ?? this.existsTimeout, visibleTimeout: visibleTimeout ?? this.visibleTimeout, settleTimeout: settleTimeout ?? this.settleTimeout, - // TODO: remove after andSettle is removed, see #1369 (https://github.com/leancodepl/patrol/issues/1369) - // ignore: deprecated_member_use_from_same_package - andSettle: andSettle ?? this.andSettle, settlePolicy: settlePolicy ?? this.settlePolicy, dragDuration: dragDuration ?? this.dragDuration, ); @@ -245,7 +232,8 @@ class PatrolTester { /// ``` /// /// This method automatically calls [WidgetTester.pumpAndSettle] after - /// tapping. If you want to disable this behavior, set [andSettle] to false. + /// tapping. If you want to disable this behavior, set [settlePolicy] to + /// [SettlePolicy.noSettle]. /// /// See also: /// - [PatrolFinder.waitUntilVisible], which is used to wait for the widget @@ -253,7 +241,6 @@ class PatrolTester { /// - [WidgetController.tap] Future tap( Finder finder, { - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, @@ -264,12 +251,8 @@ class PatrolTester { timeout: visibleTimeout, ); await tester.tap(resolvedFinder.first); - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); await _performPump( - settlePolicy: settle, + settlePolicy: settlePolicy, settleTimeout: settleTimeout, ); }); @@ -302,7 +285,6 @@ class PatrolTester { /// - [WidgetController.longPress] Future longPress( Finder finder, { - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, @@ -313,12 +295,8 @@ class PatrolTester { timeout: visibleTimeout, ); await tester.longPress(resolvedFinder.first); - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); await _performPump( - settlePolicy: settle, + settlePolicy: settlePolicy, settleTimeout: settleTimeout, ); }); @@ -342,8 +320,8 @@ class PatrolTester { /// ``` /// /// This method automatically calls [WidgetTester.pumpAndSettle] after - /// entering text. If you want to disable this behavior, set [andSettle] to - /// false. + /// entering text. If you want to disable this behavior, set [settlePolicy] to + /// [SettlePolicy.noSettle]. /// /// See also: /// - [PatrolFinder.waitUntilVisible], which is used to wait for the widget @@ -352,7 +330,6 @@ class PatrolTester { Future enterText( Finder finder, String text, { - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, Duration? visibleTimeout, Duration? settleTimeout, @@ -369,12 +346,8 @@ class PatrolTester { timeout: visibleTimeout, ); await tester.enterText(resolvedFinder.first, text); - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); await _performPump( - settlePolicy: settle, + settlePolicy: settlePolicy, settleTimeout: settleTimeout, ); }); @@ -472,8 +445,8 @@ class PatrolTester { /// [PatrolTester.config]. /// /// See also: - /// * [PatrolTester.config.andSettle], which controls the default behavior if - /// [andSettle] is null + /// * [PatrolTester.config.settlePolicy], which controls the default settle + /// behavior /// * [PatrolTester.dragUntilVisible], which scrolls to visible widget, /// not only existing one. Future dragUntilExists({ @@ -483,7 +456,6 @@ class PatrolTester { int maxIteration = defaultScrollMaxIteration, Duration? settleBetweenScrollsTimeout, Duration? dragDuration, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, }) { return TestAsyncUtils.guard(() async { @@ -492,10 +464,6 @@ class PatrolTester { viewPatrolFinder = viewPatrolFinder.hitTestable().first; dragDuration ??= config.dragDuration; settleBetweenScrollsTimeout ??= config.settleBetweenScrollsTimeout; - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); var iterationsLeft = maxIteration; while (iterationsLeft > 0 && finder.evaluate().isEmpty) { @@ -505,7 +473,7 @@ class PatrolTester { dragDuration!, ); await _performPump( - settlePolicy: settle, + settlePolicy: settlePolicy, settleTimeout: settleBetweenScrollsTimeout, ); iterationsLeft -= 1; @@ -561,7 +529,6 @@ class PatrolTester { int maxIteration = defaultScrollMaxIteration, Duration? settleBetweenScrollsTimeout, Duration? dragDuration, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, }) { return TestAsyncUtils.guard(() async { @@ -570,10 +537,6 @@ class PatrolTester { viewPatrolFinder = viewPatrolFinder.hitTestable().first; dragDuration ??= config.dragDuration; settleBetweenScrollsTimeout ??= config.settleBetweenScrollsTimeout; - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); var iterationsLeft = maxIteration; while (iterationsLeft > 0 && finder.hitTestable().evaluate().isEmpty) { @@ -583,7 +546,7 @@ class PatrolTester { dragDuration!, ); await _performPump( - settlePolicy: settle, + settlePolicy: settlePolicy, settleTimeout: settleBetweenScrollsTimeout, ); iterationsLeft -= 1; @@ -616,7 +579,6 @@ class PatrolTester { int maxScrolls = defaultScrollMaxIteration, Duration? settleBetweenScrollsTimeout, Duration? dragDuration, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, }) async { assert(maxScrolls > 0, 'maxScrolls must be positive number'); @@ -654,10 +616,6 @@ class PatrolTester { break; } - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); final resolvedFinder = await dragUntilExists( finder: finder, view: scrollablePatrolFinder.first, @@ -665,7 +623,7 @@ class PatrolTester { maxIteration: maxScrolls, settleBetweenScrollsTimeout: settleBetweenScrollsTimeout, dragDuration: dragDuration, - settlePolicy: settle, + settlePolicy: settlePolicy, ); return resolvedFinder; @@ -690,7 +648,6 @@ class PatrolTester { int maxScrolls = defaultScrollMaxIteration, Duration? settleBetweenScrollsTimeout, Duration? dragDuration, - @Deprecated('Use settlePolicy argument instead') bool? andSettle, SettlePolicy? settlePolicy, }) async { assert(maxScrolls > 0, 'maxScrolls must be positive number'); @@ -728,10 +685,6 @@ class PatrolTester { break; } - final settle = chooseSettlePolicy( - andSettle: andSettle, - settlePolicy: settlePolicy, - ); final resolvedFinder = await dragUntilVisible( finder: finder, view: scrollablePatrolFinder.first, @@ -739,7 +692,7 @@ class PatrolTester { maxIteration: maxScrolls, settleBetweenScrollsTimeout: settleBetweenScrollsTimeout, dragDuration: dragDuration, - settlePolicy: settle, + settlePolicy: settlePolicy, ); return resolvedFinder; diff --git a/packages/patrol_finders/lib/src/custom_finders/utils.dart b/packages/patrol_finders/lib/src/custom_finders/utils.dart index c1601f88e..ee42e333a 100644 --- a/packages/patrol_finders/lib/src/custom_finders/utils.dart +++ b/packages/patrol_finders/lib/src/custom_finders/utils.dart @@ -1,5 +1,3 @@ -import 'package:patrol_finders/src/custom_finders/patrol_tester.dart'; - /// Makes it possible to retrieve a name that this [Symbol] was created with. extension SymbolName on Symbol { /// Returns the name that this [Symbol] was created with. @@ -11,21 +9,3 @@ extension SymbolName on Symbol { return symbol.substring(8, symbol.length - 2); } } - -/// Returns correct [settlePolicy], regardless which settling argument was set -SettlePolicy? chooseSettlePolicy({ - bool? andSettle, - SettlePolicy? settlePolicy, -}) { - SettlePolicy? settle; - if (andSettle == null) { - settle = settlePolicy; - } else { - if (andSettle) { - settle = SettlePolicy.settle; - } else { - settle = SettlePolicy.noSettle; - } - } - return settle; -} diff --git a/packages/patrol_finders/pubspec.yaml b/packages/patrol_finders/pubspec.yaml index 012433a08..654f5e82f 100644 --- a/packages/patrol_finders/pubspec.yaml +++ b/packages/patrol_finders/pubspec.yaml @@ -6,8 +6,8 @@ repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues environment: - sdk: '>=2.18.0 <4.0.0' - flutter: '>=3.3.0' + sdk: '>=3.1.0 <4.0.0' # TODO: https://github.com/leancodepl/patrol/issues/1893 + flutter: '>=3.16.0-0' dependencies: flutter: diff --git a/packages/patrol_finders/test/patrol_tester_test.dart b/packages/patrol_finders/test/patrol_tester_test.dart index fc90718b3..e5a813090 100644 --- a/packages/patrol_finders/test/patrol_tester_test.dart +++ b/packages/patrol_finders/test/patrol_tester_test.dart @@ -1039,13 +1039,12 @@ void main() { expect($('count: 1'), findsOneWidget); }); - patrolWidgetTest('is not used by default', ($) async { + patrolWidgetTest('is used by default', ($) async { await $.pumpWidget(appWithInfiniteAnimation); - await expectLater( - () => $(ElevatedButton).tap(), - throwsFlutterError, - ); + await $('count: 0').waitUntilVisible(); + await $(ElevatedButton).tap(); + await $('count: 1').waitUntilVisible(); }); }); }); diff --git a/packages/patrol_finders/test/smoke_test.dart b/packages/patrol_finders/test/smoke_test.dart index 21e5bd967..118b199b0 100644 --- a/packages/patrol_finders/test/smoke_test.dart +++ b/packages/patrol_finders/test/smoke_test.dart @@ -100,7 +100,7 @@ void main() { isA().having( (error) => error.message, 'message', - "Finder \"zero widgets with key [<'someWrongKey'>] (ignoring offstage widgets)\" found no widgets", + "Finder \"Found 0 widgets with key [<'someWrongKey'>]: []\" found no widgets", ), ), ); diff --git a/packages/patrol_gen/pubspec.lock b/packages/patrol_gen/pubspec.lock index a7f24b26e..a64ae0a2d 100644 --- a/packages/patrol_gen/pubspec.lock +++ b/packages/patrol_gen/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "65.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" args: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" file: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" glob: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index df13799a6..a8b683445 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: location_workspace environment: - sdk: '>=2.18.0 <4.0.0' + sdk: '>=3.2.0-0 <4.0.0' dev_dependencies: melos: ^3.1.1 diff --git a/schema.dart b/schema.dart index d117a8707..a18e919ef 100644 --- a/schema.dart +++ b/schema.dart @@ -69,6 +69,14 @@ class GetNativeViewsRequest { late String appId; } +class GetNativeUITreeRequest { + List? iosInstalledApps; +} + +class GetNativeUITreeRespone { + late List roots; +} + class NativeView { String? className; String? text; @@ -178,6 +186,7 @@ abstract class NativeAutomator { void openQuickSettings(OpenQuickSettingsRequest request); // general UI interaction + GetNativeUITreeRespone getNativeUITree(GetNativeUITreeRequest request); GetNativeViewsResponse getNativeViews(GetNativeViewsRequest request); void tap(TapRequest request); void doubleTap(TapRequest request);