diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d2eda35897db7..4bd59360f36c3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,4 +17,4 @@ jobs: runs-on: ubuntu-latest steps: # Source available at https://github.com/actions/labeler/blob/main/README.md - - uses: actions/labeler@6b107e7a7ee5e054e0bcce60de5181d21e2f00fb + - uses: actions/labeler@2713f7303c96cb1e69627957ec16eea0fd7f94a4 diff --git a/bin/internal/engine.version b/bin/internal/engine.version index 6272c6e0d96a2..10cf910cdbdb6 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -168b0bf3f70d3ad24fb35f4c4731d8ebf67606c7 +eebcf36cd38fb5c4feba6acf2a9f44c268278463 diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 39757c3a089ac..f9ddc96ae315b 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -83959fb7de37628c8fcb8a66d8660816047b3242 +d449a17f8706850388a16d8acc72398c2118cf9a diff --git a/bin/internal/fuchsia-linux.version b/bin/internal/fuchsia-linux.version index 9b489bad9d70a..b3135d9a3fec6 100644 --- a/bin/internal/fuchsia-linux.version +++ b/bin/internal/fuchsia-linux.version @@ -1 +1 @@ -Zbd3haNY2Idcdu1Fjx-fL_Omg3h7uVv5ejesNRIqVD4C +-dXJ_pnUVwmjscIYm42_OE80nLeouKgHGRsqBD2JoFoC diff --git a/bin/internal/fuchsia-mac.version b/bin/internal/fuchsia-mac.version index 7f18a6fb7767f..36cc8e60a57b3 100644 --- a/bin/internal/fuchsia-mac.version +++ b/bin/internal/fuchsia-mac.version @@ -1 +1 @@ -DzmjiSg6XC0JUfbKPkKaUvxaz0ozfJCW3uSRhvXtp_wC +aAjEDVse7qfMt0NqE-x2K0y-JIcvU2XLT8vjCHu2QOsC diff --git a/dev/devicelab/lib/framework/talkback.dart b/dev/devicelab/lib/framework/talkback.dart new file mode 100644 index 0000000000000..2edfa9eaaacde --- /dev/null +++ b/dev/devicelab/lib/framework/talkback.dart @@ -0,0 +1,79 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:path/path.dart' as path; +import 'package:pub_semver/pub_semver.dart'; + +String adbPath() { + final String? androidHome = io.Platform.environment['ANDROID_HOME'] ?? io.Platform.environment['ANDROID_SDK_ROOT']; + if (androidHome == null) { + return 'adb'; + } else { + return path.join(androidHome, 'platform-tools', 'adb'); + } +} + +Future getTalkbackVersion() async { + final io.ProcessResult result = await io.Process.run(adbPath(), const [ + 'shell', + 'dumpsys', + 'package', + 'com.google.android.marvin.talkback', + ]); + if (result.exitCode != 0) { + throw Exception('Failed to get TalkBack version: ${result.stdout as String}\n${result.stderr as String}'); + } + final List lines = (result.stdout as String).split('\n'); + String? version; + for (final String line in lines) { + if (line.contains('versionName')) { + version = line.replaceAll(RegExp(r'\s*versionName='), ''); + break; + } + } + if (version == null) { + throw Exception('Unable to determine TalkBack version.'); + } + + // Android doesn't quite use semver, so convert the version string to semver form. + final RegExp startVersion = RegExp(r'(?\d+)\.(?\d+)\.(?\d+)(\.(?\d+))?'); + final RegExpMatch? match = startVersion.firstMatch(version); + if (match == null) { + return Version(0, 0, 0); + } + return Version( + int.parse(match.namedGroup('major')!), + int.parse(match.namedGroup('minor')!), + int.parse(match.namedGroup('patch')!), + build: match.namedGroup('build'), + ); +} + +Future enableTalkBack() async { + final io.Process run = await io.Process.start(adbPath(), const [ + 'shell', + 'settings', + 'put', + 'secure', + 'enabled_accessibility_services', + 'com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService', + ]); + await run.exitCode; + + print('TalkBack version is ${await getTalkbackVersion()}'); +} + +Future disableTalkBack() async { + final io.Process run = await io.Process.start(adbPath(), const [ + 'shell', + 'settings', + 'put', + 'secure', + 'enabled_accessibility_services', + 'null', + ]); + await run.exitCode; +} diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart index d663a2361a55a..0172d576fe417 100644 --- a/dev/devicelab/lib/tasks/integration_tests.dart +++ b/dev/devicelab/lib/tasks/integration_tests.dart @@ -4,6 +4,7 @@ import '../framework/devices.dart'; import '../framework/framework.dart'; +import '../framework/talkback.dart'; import '../framework/task_result.dart'; import '../framework/utils.dart'; @@ -74,9 +75,10 @@ TaskFunction createHybridAndroidViewsIntegrationTest() { } TaskFunction createAndroidSemanticsIntegrationTest() { - return DriverTest( + return IntegrationTest( '${flutterDirectory.path}/dev/integration_tests/android_semantics_testing', - 'lib/main.dart', + 'integration_test/main_test.dart', + withTalkBack: true, ).call; } @@ -213,6 +215,7 @@ class IntegrationTest { this.testTarget, { this.extraOptions = const [], this.createPlatforms = const [], + this.withTalkBack = false, } ); @@ -220,6 +223,7 @@ class IntegrationTest { final String testTarget; final List extraOptions; final List createPlatforms; + final bool withTalkBack; Future call() { return inDirectory(testDirectory, () async { @@ -237,6 +241,13 @@ class IntegrationTest { ]); } + if (withTalkBack) { + if (device is! AndroidDevice) { + return TaskResult.failure('A test that enables TalkBack can only be run on Android devices'); + } + await enableTalkBack(); + } + final List options = [ '-v', '-d', @@ -246,6 +257,10 @@ class IntegrationTest { ]; await flutter('test', options: options); + if (withTalkBack) { + await disableTalkBack(); + } + return TaskResult.success(null); }); } diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java b/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java index e8db455923fe6..c623a84e3fbaa 100644 --- a/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java +++ b/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java @@ -20,7 +20,6 @@ import android.content.Context; import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterView; @@ -123,11 +122,60 @@ private Map convertSemantics(AccessibilityNodeInfo node, int id) if (actionList.size() > 0) { ArrayList actions = new ArrayList<>(); for (AccessibilityNodeInfo.AccessibilityAction action : actionList) { - actions.add(action.getId()); + if (kIdByAction.containsKey(action)) { + actions.add(kIdByAction.get(action).intValue()); + } } result.put("actions", actions); } return result; } } + + // These indices need to be in sync with android_semantics_testing/lib/src/constants.dart + static final int kFocusIndex = 1 << 0; + static final int kClearFocusIndex = 1 << 1; + static final int kSelectIndex = 1 << 2; + static final int kClearSelectionIndex = 1 << 3; + static final int kClickIndex = 1 << 4; + static final int kLongClickIndex = 1 << 5; + static final int kAccessibilityFocusIndex = 1 << 6; + static final int kClearAccessibilityFocusIndex = 1 << 7; + static final int kNextAtMovementGranularityIndex = 1 << 8; + static final int kPreviousAtMovementGranularityIndex = 1 << 9; + static final int kNextHtmlElementIndex = 1 << 10; + static final int kPreviousHtmlElementIndex = 1 << 11; + static final int kScrollForwardIndex = 1 << 12; + static final int kScrollBackwardIndex = 1 << 13; + static final int kCutIndex = 1 << 14; + static final int kCopyIndex = 1 << 15; + static final int kPasteIndex = 1 << 16; + static final int kSetSelectionIndex = 1 << 17; + static final int kExpandIndex = 1 << 18; + static final int kCollapseIndex = 1 << 19; + static final int kSetText = 1 << 21; + + static final Map kIdByAction = new HashMap() {{ + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_FOCUS, kFocusIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_FOCUS, kClearFocusIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_SELECT, kSelectIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_SELECTION, kClearSelectionIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK, kClickIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK, kLongClickIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS, kAccessibilityFocusIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS, kClearAccessibilityFocusIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, kNextAtMovementGranularityIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, kPreviousAtMovementGranularityIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_NEXT_HTML_ELEMENT, kNextHtmlElementIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_PREVIOUS_HTML_ELEMENT, kPreviousHtmlElementIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD, kScrollForwardIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD, kScrollBackwardIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_CUT, kCutIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_COPY, kCopyIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_PASTE, kPasteIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_SELECTION, kSetSelectionIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND, kExpandIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE, kCollapseIndex); + put(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT, kSetText); + }}; } diff --git a/dev/integration_tests/android_semantics_testing/android/project-app.lockfile b/dev/integration_tests/android_semantics_testing/android/project-app.lockfile index b5b0b745c3536..df07b3a3917bc 100644 --- a/dev/integration_tests/android_semantics_testing/android/project-app.lockfile +++ b/dev/integration_tests/android_semantics_testing/android/project-app.lockfile @@ -18,11 +18,16 @@ androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath,debu androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.savedstate:savedstate:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.window:window-java:1.0.0-beta03=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath -androidx.window:window:1.0.0-beta03=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.window:window-java:1.0.0-beta04=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.window:window:1.0.0-beta04=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath com.android.tools.analytics-library:protos:27.1.3=lintClassPath com.android.tools.analytics-library:shared:27.1.3=lintClassPath com.android.tools.analytics-library:tracker:27.1.3=lintClassPath @@ -54,16 +59,19 @@ com.android.tools:sdk-common:27.1.3=lintClassPath com.android.tools:sdklib:27.1.3=lintClassPath com.android:signflinger:4.1.3=lintClassPath com.android:zipflinger:4.1.3=lintClassPath -com.google.code.findbugs:jsr305:3.0.2=lintClassPath +com.google.code.findbugs:jsr305:2.0.1=debugAndroidTestCompileClasspath +com.google.code.findbugs:jsr305:3.0.2=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath com.google.code.gson:gson:2.8.5=lintClassPath -com.google.errorprone:error_prone_annotations:2.3.2=lintClassPath -com.google.guava:failureaccess:1.0.1=lintClassPath +com.google.errorprone:error_prone_annotations:2.3.2=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:failureaccess:1.0.1=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:guava:28.1-android=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath com.google.guava:guava:28.1-jre=lintClassPath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=lintClassPath -com.google.j2objc:j2objc-annotations:1.3=lintClassPath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath +com.google.j2objc:j2objc-annotations:1.3=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath com.google.jimfs:jimfs:1.1=lintClassPath com.google.protobuf:protobuf-java:3.10.0=lintClassPath com.googlecode.json-simple:json-simple:1.1=lintClassPath +com.squareup:javawriter:2.1.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath com.squareup:javawriter:2.5.0=lintClassPath com.sun.activation:javax.activation:1.2.0=lintClassPath com.sun.istack:istack-commons-runtime:3.0.7=lintClassPath @@ -72,21 +80,26 @@ commons-codec:commons-codec:1.10=lintClassPath commons-logging:commons-logging:1.2=lintClassPath it.unimi.dsi:fastutil:7.2.0=lintClassPath javax.activation:javax.activation-api:1.2.0=lintClassPath -javax.inject:javax.inject:1=lintClassPath +javax.inject:javax.inject:1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath javax.xml.bind:jaxb-api:2.3.1=lintClassPath +junit:junit:4.12=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath net.sf.jopt-simple:jopt-simple:4.9=lintClassPath -net.sf.kxml:kxml2:2.3.0=lintClassPath +net.sf.kxml:kxml2:2.3.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.apache.commons:commons-compress:1.12=lintClassPath org.apache.httpcomponents:httpclient:4.5.6=lintClassPath org.apache.httpcomponents:httpcore:4.4.10=lintClassPath org.apache.httpcomponents:httpmime:4.5.6=lintClassPath org.bouncycastle:bcpkix-jdk15on:1.56=lintClassPath org.bouncycastle:bcprov-jdk15on:1.56=lintClassPath +org.checkerframework:checker-compat-qual:2.5.5=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath org.checkerframework:checker-qual:2.8.1=lintClassPath org.codehaus.groovy:groovy-all:2.4.15=lintClassPath -org.codehaus.mojo:animal-sniffer-annotations:1.18=lintClassPath +org.codehaus.mojo:animal-sniffer-annotations:1.18=debugRuntimeClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileRuntimeClasspath,profileUnitTestRuntimeClasspath,releaseRuntimeClasspath,releaseUnitTestRuntimeClasspath org.glassfish.jaxb:jaxb-runtime:2.3.1=lintClassPath org.glassfish.jaxb:txw2:2.3.1=lintClassPath +org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains.kotlin:kotlin-reflect:1.3.72=lintClassPath org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72=lintClassPath org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath diff --git a/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile b/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile new file mode 100644 index 0000000000000..9d48dc0b092dd --- /dev/null +++ b/dev/integration_tests/android_semantics_testing/android/project-integration_test.lockfile @@ -0,0 +1,122 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +androidx.activity:activity:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation-experimental:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.annotation:annotation:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.arch.core:core-common:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.arch.core:core-runtime:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.collection:collection:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.core:core:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.fragment:fragment:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common-java8:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-common:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata-core:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-livedata:2.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-runtime:2.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.savedstate:savedstate:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-core:3.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test.espresso:espresso-idling-resource:3.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:monitor:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:rules:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.test:runner:1.2.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.tracing:tracing:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.versionedparcelable:versionedparcelable:1.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.viewpager:viewpager:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.window:window-java:1.0.0-beta04=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +androidx.window:window:1.0.0-beta04=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.android.tools.analytics-library:protos:27.1.3=lintClassPath +com.android.tools.analytics-library:shared:27.1.3=lintClassPath +com.android.tools.analytics-library:tracker:27.1.3=lintClassPath +com.android.tools.build:aapt2-proto:4.1.0-alpha01-6193524=lintClassPath +com.android.tools.build:aapt2:4.1.3-6503028=_internal_aapt2_binary +com.android.tools.build:apksig:4.1.3=lintClassPath +com.android.tools.build:apkzlib:4.1.3=lintClassPath +com.android.tools.build:builder-model:4.1.3=lintClassPath +com.android.tools.build:builder-test-api:4.1.3=lintClassPath +com.android.tools.build:builder:4.1.3=lintClassPath +com.android.tools.build:gradle-api:4.1.3=lintClassPath +com.android.tools.build:manifest-merger:27.1.3=lintClassPath +com.android.tools.ddms:ddmlib:27.1.3=lintClassPath +com.android.tools.external.com-intellij:intellij-core:27.1.3=lintClassPath +com.android.tools.external.com-intellij:kotlin-compiler:27.1.3=lintClassPath +com.android.tools.external.org-jetbrains:uast:27.1.3=lintClassPath +com.android.tools.layoutlib:layoutlib-api:27.1.3=lintClassPath +com.android.tools.lint:lint-api:27.1.3=lintClassPath +com.android.tools.lint:lint-checks:27.1.3=lintClassPath +com.android.tools.lint:lint-gradle-api:27.1.3=lintClassPath +com.android.tools.lint:lint-gradle:27.1.3=lintClassPath +com.android.tools.lint:lint-model:27.1.3=lintClassPath +com.android.tools.lint:lint:27.1.3=lintClassPath +com.android.tools:annotations:27.1.3=lintClassPath +com.android.tools:common:27.1.3=lintClassPath +com.android.tools:dvlib:27.1.3=lintClassPath +com.android.tools:repository:27.1.3=lintClassPath +com.android.tools:sdk-common:27.1.3=lintClassPath +com.android.tools:sdklib:27.1.3=lintClassPath +com.android:signflinger:4.1.3=lintClassPath +com.android:zipflinger:4.1.3=lintClassPath +com.google.code.findbugs:jsr305:3.0.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.code.gson:gson:2.8.5=lintClassPath +com.google.errorprone:error_prone_annotations:2.3.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:failureaccess:1.0.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:guava:28.1-android=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.guava:guava:28.1-jre=lintClassPath +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.j2objc:j2objc-annotations:1.3=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.google.jimfs:jimfs:1.1=lintClassPath +com.google.protobuf:protobuf-java:3.10.0=lintClassPath +com.googlecode.json-simple:json-simple:1.1=lintClassPath +com.squareup:javawriter:2.1.1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +com.squareup:javawriter:2.5.0=lintClassPath +com.sun.activation:javax.activation:1.2.0=lintClassPath +com.sun.istack:istack-commons-runtime:3.0.7=lintClassPath +com.sun.xml.fastinfoset:FastInfoset:1.2.15=lintClassPath +commons-codec:commons-codec:1.10=lintClassPath +commons-logging:commons-logging:1.2=lintClassPath +it.unimi.dsi:fastutil:7.2.0=lintClassPath +javax.activation:javax.activation-api:1.2.0=lintClassPath +javax.inject:javax.inject:1=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +javax.xml.bind:jaxb-api:2.3.1=lintClassPath +junit:junit:4.12=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +net.sf.jopt-simple:jopt-simple:4.9=lintClassPath +net.sf.kxml:kxml2:2.3.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.apache.commons:commons-compress:1.12=lintClassPath +org.apache.httpcomponents:httpclient:4.5.6=lintClassPath +org.apache.httpcomponents:httpcore:4.4.10=lintClassPath +org.apache.httpcomponents:httpmime:4.5.6=lintClassPath +org.bouncycastle:bcpkix-jdk15on:1.56=lintClassPath +org.bouncycastle:bcprov-jdk15on:1.56=lintClassPath +org.checkerframework:checker-compat-qual:2.5.5=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.checkerframework:checker-qual:2.8.1=lintClassPath +org.codehaus.groovy:groovy-all:2.4.15=lintClassPath +org.codehaus.mojo:animal-sniffer-annotations:1.18=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.glassfish.jaxb:jaxb-runtime:2.3.1=lintClassPath +org.glassfish.jaxb:txw2:2.3.1=lintClassPath +org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.hamcrest:hamcrest-integration:1.3=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.hamcrest:hamcrest-library:1.3=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-reflect:1.3.72=lintClassPath +org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72=lintClassPath +org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72=lintClassPath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72=lintClassPath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.3.72=lintClassPath +org.jetbrains.kotlin:kotlin-stdlib:1.5.31=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jetbrains.trove4j:trove4j:20160824=lintClassPath +org.jetbrains:annotations:13.0=debugAndroidTestCompileClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,lintClassPath,profileCompileClasspath,profileRuntimeClasspath,profileUnitTestCompileClasspath,profileUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jvnet.staxex:stax-ex:1.8=lintClassPath +org.ow2.asm:asm-analysis:7.0=lintClassPath +org.ow2.asm:asm-commons:7.0=lintClassPath +org.ow2.asm:asm-tree:7.0=lintClassPath +org.ow2.asm:asm-util:7.0=lintClassPath +org.ow2.asm:asm:7.0=lintClassPath +empty=androidApis,androidTestUtil,compile,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAnnotationProcessorClasspath,debugUnitTestAnnotationProcessorClasspath,lintChecks,lintPublish,profileAnnotationProcessorClasspath,profileUnitTestAnnotationProcessorClasspath,releaseAnnotationProcessorClasspath,releaseUnitTestAnnotationProcessorClasspath,testCompile diff --git a/dev/integration_tests/android_semantics_testing/integration_test/main_test.dart b/dev/integration_tests/android_semantics_testing/integration_test/main_test.dart new file mode 100644 index 0000000000000..981a814e08934 --- /dev/null +++ b/dev/integration_tests/android_semantics_testing/integration_test/main_test.dart @@ -0,0 +1,677 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:android_semantics_testing/android_semantics_testing.dart'; +import 'package:android_semantics_testing/main.dart' as app; +import 'package:android_semantics_testing/test_constants.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +// The accessibility focus actions are added when a semantics node receives or +// lose accessibility focus. This test ignores these actions since it is hard to +// predict which node has the accessibility focus after a screen changes. +const List ignoredAccessibilityFocusActions = [ + AndroidSemanticsAction.accessibilityFocus, + AndroidSemanticsAction.clearAccessibilityFocus, +]; + +const MethodChannel kSemanticsChannel = MethodChannel('semantics'); + +Future setClipboard(String message) async { + final Completer completer = Completer(); + Future completeSetClipboard([Object? _]) async { + await kSemanticsChannel.invokeMethod('setClipboard', { + 'message': message, + }); + completer.complete(); + } + if (SchedulerBinding.instance.hasScheduledFrame) { + SchedulerBinding.instance.addPostFrameCallback(completeSetClipboard); + } else { + completeSetClipboard(); + } + await completer.future; +} + +Future getSemantics(Finder finder, WidgetTester tester) async { + final int id = tester.getSemantics(finder).id; + final Completer completer = Completer(); + Future completeSemantics([Object? _]) async { + final dynamic result = await kSemanticsChannel.invokeMethod('getSemanticsNode', { + 'id': id, + }); + completer.complete(json.encode(result)); + } + if (SchedulerBinding.instance.hasScheduledFrame) { + SchedulerBinding.instance.addPostFrameCallback(completeSemantics); + } else { + completeSemantics(); + } + return AndroidSemanticsNode.deserialize(await completer.future); +} + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('AccessibilityBridge', () { + group('TextField', () { + Future prepareTextField(WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(find.text(textFieldRoute)); + await tester.pumpAndSettle(); + + // The text selection menu and related semantics vary depending on if + // the clipboard contents are pasteable. Copy some text into the + // clipboard to make sure these tests always run with pasteable content + // in the clipboard. + // Ideally this should test the case where there is nothing on the + // clipboard as well, but there is no reliable way to clear the + // clipboard on Android devices. + await setClipboard('Hello World'); + await tester.pumpAndSettle(); + } + + testWidgets('TextField has correct Android semantics', (WidgetTester tester) async { + final Finder normalTextField = find.descendant( + of: find.byKey(const ValueKey(normalTextFieldKeyValue)), + matching: find.byType(EditableText), + ); + + await prepareTextField(tester); + expect( + await getSemantics(normalTextField, tester), + hasAndroidSemantics( + className: AndroidClassName.editText, + isEditable: true, + isFocusable: true, + isFocused: false, + isPassword: false, + actions: [ + AndroidSemanticsAction.click, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + await tester.tap(normalTextField); + await tester.pumpAndSettle(); + + expect( + await getSemantics(normalTextField, tester), + hasAndroidSemantics( + className: AndroidClassName.editText, + isFocusable: true, + isFocused: true, + isEditable: true, + isPassword: false, + actions: [ + AndroidSemanticsAction.click, + AndroidSemanticsAction.paste, + AndroidSemanticsAction.setSelection, + AndroidSemanticsAction.setText, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + + await tester.enterText(normalTextField, 'hello world'); + await tester.pumpAndSettle(); + + expect( + await getSemantics(normalTextField, tester), + hasAndroidSemantics( + text: 'hello world', + className: AndroidClassName.editText, + isFocusable: true, + isFocused: true, + isEditable: true, + isPassword: false, + actions: [ + AndroidSemanticsAction.click, + AndroidSemanticsAction.paste, + AndroidSemanticsAction.setSelection, + AndroidSemanticsAction.setText, + AndroidSemanticsAction.previousAtMovementGranularity, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + }, timeout: Timeout.none); + + testWidgets('password TextField has correct Android semantics', (WidgetTester tester) async { + final Finder passwordTextField = find.descendant( + of: find.byKey(const ValueKey(passwordTextFieldKeyValue)), + matching: find.byType(EditableText), + ); + + await prepareTextField(tester); + expect( + await getSemantics(passwordTextField, tester), + hasAndroidSemantics( + className: AndroidClassName.editText, + isEditable: true, + isFocusable: true, + isFocused: false, + isPassword: true, + actions: [ + AndroidSemanticsAction.click, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + + await tester.tap(passwordTextField); + await tester.pumpAndSettle(); + + expect( + await getSemantics(passwordTextField, tester), + hasAndroidSemantics( + className: AndroidClassName.editText, + isFocusable: true, + isFocused: true, + isEditable: true, + isPassword: true, + actions: [ + AndroidSemanticsAction.click, + AndroidSemanticsAction.paste, + AndroidSemanticsAction.setSelection, + AndroidSemanticsAction.setText, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + + await tester.enterText(passwordTextField, 'hello world'); + await tester.pumpAndSettle(); + + expect( + await getSemantics(passwordTextField, tester), + hasAndroidSemantics( + text: '\u{2022}' * ('hello world'.length), + className: AndroidClassName.editText, + isFocusable: true, + isFocused: true, + isEditable: true, + isPassword: true, + actions: [ + AndroidSemanticsAction.click, + AndroidSemanticsAction.paste, + AndroidSemanticsAction.setSelection, + AndroidSemanticsAction.setText, + AndroidSemanticsAction.previousAtMovementGranularity, + ], + // We can't predict the a11y focus when the screen changes. + ignoredActions: ignoredAccessibilityFocusActions, + ), + ); + }, timeout: Timeout.none); + }); + + group('SelectionControls', () { + Future prepareSelectionControls(WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(find.text(selectionControlsRoute)); + await tester.pumpAndSettle(); + } + + testWidgets('Checkbox has correct Android semantics', (WidgetTester tester) async { + final Finder checkbox = find.byKey(const ValueKey(checkboxKeyValue)); + final Finder disabledCheckbox = find.byKey(const ValueKey(disabledCheckboxKeyValue)); + + await prepareSelectionControls(tester); + expect( + await getSemantics(checkbox, tester), + hasAndroidSemantics( + className: AndroidClassName.checkBox, + isChecked: false, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(checkbox); + await tester.pumpAndSettle(); + + expect( + await getSemantics(checkbox, tester), + hasAndroidSemantics( + className: AndroidClassName.checkBox, + isChecked: true, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + expect( + await getSemantics(disabledCheckbox, tester), + hasAndroidSemantics( + className: AndroidClassName.checkBox, + isCheckable: true, + isEnabled: false, + ignoredActions: ignoredAccessibilityFocusActions, + actions: const [], + ), + ); + }, timeout: Timeout.none); + + testWidgets('Radio has correct Android semantics', (WidgetTester tester) async { + final Finder radio = find.byKey(const ValueKey(radio2KeyValue)); + + await prepareSelectionControls(tester); + expect( + await getSemantics(radio, tester), + hasAndroidSemantics( + className: AndroidClassName.radio, + isChecked: false, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(radio); + await tester.pumpAndSettle(); + + expect( + await getSemantics(radio, tester), + hasAndroidSemantics( + className: AndroidClassName.radio, + isChecked: true, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + }, timeout: Timeout.none); + + testWidgets('Switch has correct Android semantics', (WidgetTester tester) async { + final Finder switchFinder = find.byKey(const ValueKey(switchKeyValue)); + + await prepareSelectionControls(tester); + expect( + await getSemantics(switchFinder, tester), + hasAndroidSemantics( + className: AndroidClassName.toggleSwitch, + isChecked: false, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(switchFinder); + await tester.pumpAndSettle(); + + expect( + await getSemantics(switchFinder, tester), + hasAndroidSemantics( + className: AndroidClassName.toggleSwitch, + isChecked: true, + isCheckable: true, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + }, timeout: Timeout.none); + + // Regression test for https://github.com/flutter/flutter/issues/20820. + testWidgets('Switch can be labeled', (WidgetTester tester) async { + final Finder switchFinder = find.byKey(const ValueKey(labeledSwitchKeyValue)); + + await prepareSelectionControls(tester); + expect( + await getSemantics(switchFinder, tester), + hasAndroidSemantics( + className: AndroidClassName.toggleSwitch, + isChecked: false, + isCheckable: true, + isEnabled: true, + isFocusable: true, + contentDescription: switchLabel, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + }, timeout: Timeout.none); + }); + + group('Popup Controls', () { + Future preparePopupControls(WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(find.text(popupControlsRoute)); + await tester.pumpAndSettle(); + } + + testWidgets('Popup Menu has correct Android semantics', (WidgetTester tester) async { + final Finder popupButton = find.byKey(const ValueKey(popupButtonKeyValue)); + + await preparePopupControls(tester); + expect( + await getSemantics(popupButton, tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(popupButton); + await tester.pumpAndSettle(); + + try { + for (final String item in popupItems) { + expect( + await getSemantics(find.byKey(ValueKey('$popupKeyValue.$item')), tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Popup $item doesn't have the right semantics", + ); + } + await tester.tap(find.byKey(ValueKey('$popupKeyValue.${popupItems.first}'))); + await tester.pumpAndSettle(); + + // Pop up the menu again, to verify that TalkBack gets the right answer + // more than just the first time. + await tester.tap(popupButton); + await tester.pumpAndSettle(); + + for (final String item in popupItems) { + expect( + await getSemantics(find.byKey(ValueKey('$popupKeyValue.$item')), tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Popup $item doesn't have the right semantics the second time", + ); + } + } finally { + await tester.tap(find.byKey(ValueKey('$popupKeyValue.${popupItems.first}'))); + } + }, timeout: Timeout.none); + + testWidgets('Dropdown Menu has correct Android semantics', (WidgetTester tester) async { + final Finder dropdownButton = find.byKey(const ValueKey(dropdownButtonKeyValue)); + + await preparePopupControls(tester); + expect( + await getSemantics(dropdownButton, tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(dropdownButton); + await tester.pumpAndSettle(); + + try { + for (final String item in popupItems) { + // There are two copies of each item, so we want to find the version + // that is in the overlay, not the one in the dropdown. + expect( + await getSemantics( + find.descendant( + of: find.byType(Scrollable), + matching: find.byKey(ValueKey('$dropdownKeyValue.$item')), + ), + tester, + ), + hasAndroidSemantics( + className: AndroidClassName.view, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Dropdown $item doesn't have the right semantics", + ); + } + await tester.tap( + find.descendant( + of: find.byType(Scrollable), + matching: find.byKey(ValueKey('$dropdownKeyValue.${popupItems.first}')), + ), + ); + await tester.pumpAndSettle(); + + // Pop up the dropdown again, to verify that TalkBack gets the right answer + // more than just the first time. + await tester.tap(dropdownButton); + await tester.pumpAndSettle(); + + for (final String item in popupItems) { + // There are two copies of each item, so we want to find the version + // that is in the overlay, not the one in the dropdown. + expect( + await getSemantics( + find.descendant( + of: find.byType(Scrollable), + matching: find.byKey(ValueKey('$dropdownKeyValue.$item')), + ), + tester, + ), + hasAndroidSemantics( + className: AndroidClassName.view, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Dropdown $item doesn't have the right semantics the second time.", + ); + } + } finally { + await tester.tap( + find.descendant( + of: find.byType(Scrollable), + matching: find.byKey(ValueKey('$dropdownKeyValue.${popupItems.first}')), + ), + ); + } + }, timeout: Timeout.none); + + testWidgets('Modal alert dialog has correct Android semantics', (WidgetTester tester) async { + final Finder alertButton = find.byKey(const ValueKey(alertButtonKeyValue)); + + await preparePopupControls(tester); + expect( + await getSemantics(alertButton, tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + ); + + await tester.tap(alertButton); + await tester.pumpAndSettle(); + + try { + expect( + await getSemantics(find.byKey(const ValueKey('$alertKeyValue.OK')), tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Alert OK button doesn't have the right semantics", + ); + + for (final String item in ['Title', 'Body1', 'Body2']) { + expect( + await getSemantics(find.byKey(ValueKey('$alertKeyValue.$item')), tester), + hasAndroidSemantics( + className: AndroidClassName.view, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [], + ), + reason: "Alert $item button doesn't have the right semantics", + ); + } + + await tester.tap(find.byKey(const ValueKey('$alertKeyValue.OK'))); + await tester.pumpAndSettle(); + + // Pop up the alert again, to verify that TalkBack gets the right answer + // more than just the first time. + await tester.tap(alertButton); + await tester.pumpAndSettle(); + + expect( + await getSemantics(find.byKey(const ValueKey('$alertKeyValue.OK')), tester), + hasAndroidSemantics( + className: AndroidClassName.button, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [ + AndroidSemanticsAction.click, + ], + ), + reason: "Alert OK button doesn't have the right semantics", + ); + + for (final String item in ['Title', 'Body1', 'Body2']) { + expect( + await getSemantics(find.byKey(ValueKey('$alertKeyValue.$item')), tester), + hasAndroidSemantics( + className: AndroidClassName.view, + isChecked: false, + isCheckable: false, + isEnabled: true, + isFocusable: true, + ignoredActions: ignoredAccessibilityFocusActions, + actions: [], + ), + reason: "Alert $item button doesn't have the right semantics", + ); + } + } finally { + await tester.tap(find.byKey(const ValueKey('$alertKeyValue.OK'))); + } + }, timeout: Timeout.none); + }); + + group('Headings', () { + Future prepareHeading(WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(find.text(headingsRoute)); + await tester.pumpAndSettle(); + } + + testWidgets('AppBar title has correct Android heading semantics', (WidgetTester tester) async { + await prepareHeading(tester); + expect( + await getSemantics(find.byKey(const ValueKey(appBarTitleKeyValue)), tester), + hasAndroidSemantics(isHeading: true), + ); + }, timeout: Timeout.none); + + testWidgets('body text does not have Android heading semantics', (WidgetTester tester) async { + await prepareHeading(tester); + expect( + await getSemantics(find.byKey(const ValueKey(bodyTextKeyValue)), tester), + hasAndroidSemantics(isHeading: false), + ); + }, timeout: Timeout.none); + }); + }); +} diff --git a/dev/integration_tests/android_semantics_testing/lib/main.dart b/dev/integration_tests/android_semantics_testing/lib/main.dart index 12afeada564ea..0305bac439845 100644 --- a/dev/integration_tests/android_semantics_testing/lib/main.dart +++ b/dev/integration_tests/android_semantics_testing/lib/main.dart @@ -2,13 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_driver/driver_extension.dart'; import 'src/tests/controls_page.dart'; import 'src/tests/headings_page.dart'; @@ -16,49 +10,9 @@ import 'src/tests/popup_page.dart'; import 'src/tests/text_field_page.dart'; void main() { - timeDilation = 0.05; // remove animations. - enableFlutterDriverExtension(handler: dataHandler); runApp(const TestApp()); } -const MethodChannel kSemanticsChannel = MethodChannel('semantics'); - -Future dataHandler(String? message) async { - if (message != null && message.contains('getSemanticsNode')) { - final Completer completer = Completer(); - final int id = int.tryParse(message.split('#')[1]) ?? 0; - Future completeSemantics([Object? _]) async { - final dynamic result = await kSemanticsChannel.invokeMethod('getSemanticsNode', { - 'id': id, - }); - completer.complete(json.encode(result)); - } - if (SchedulerBinding.instance.hasScheduledFrame) { - SchedulerBinding.instance.addPostFrameCallback(completeSemantics); - } else { - completeSemantics(); - } - return completer.future; - } - if (message != null && message.contains('setClipboard')) { - final Completer completer = Completer(); - final String str = message.split('#')[1]; - Future completeSetClipboard([Object? _]) async { - await kSemanticsChannel.invokeMethod('setClipboard', { - 'message': str, - }); - completer.complete(''); - } - if (SchedulerBinding.instance.hasScheduledFrame) { - SchedulerBinding.instance.addPostFrameCallback(completeSetClipboard); - } else { - completeSetClipboard(); - } - return completer.future; - } - throw UnimplementedError(); -} - Map routes = { selectionControlsRoute : (BuildContext context) => const SelectionControlsPage(), popupControlsRoute : (BuildContext context) => const PopupControlsPage(), diff --git a/dev/integration_tests/android_semantics_testing/lib/src/constants.dart b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart index 1ff8c14146da5..b3d30cf657579 100644 --- a/dev/integration_tests/android_semantics_testing/lib/src/constants.dart +++ b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart @@ -98,6 +98,7 @@ enum AndroidSemanticsAction { /// The Android id of the action. final int id; + // These indices need to be in sync with android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java static const int _kFocusIndex = 1 << 0; static const int _kClearFocusIndex = 1 << 1; static const int _kSelectIndex = 1 << 2; diff --git a/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart index 15f43f7fd6403..f650ea769fd6b 100644 --- a/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart +++ b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart @@ -77,7 +77,8 @@ class _AndroidSemanticsMatcher extends Matcher { this.isHeading, this.isPassword, this.isLongClickable, - }); + }) : assert(ignoredActions == null || actions != null, 'actions must not be null if ignoredActions is not null'), + assert(ignoredActions == null || !actions!.any(ignoredActions.contains)); final String? text; final String? className; @@ -115,6 +116,9 @@ class _AndroidSemanticsMatcher extends Matcher { if (actions != null) { description.add(' with actions: $actions'); } + if (ignoredActions != null) { + description.add(' with ignoredActions: $ignoredActions'); + } if (rect != null) { description.add(' with rect: $rect'); } @@ -170,13 +174,15 @@ class _AndroidSemanticsMatcher extends Matcher { } if (actions != null) { final List itemActions = item.getActions(); + if (ignoredActions != null) { + itemActions.removeWhere(ignoredActions!.contains); + } if (!unorderedEquals(actions!).matches(itemActions, matchState)) { final List actionsString = actions!.map((AndroidSemanticsAction action) => action.toString()).toList()..sort(); final List itemActionsString = itemActions.map((AndroidSemanticsAction action) => action.toString()).toList()..sort(); - final Set unexpected = itemActions.toSet().difference(actions!.toSet()); final Set unexpectedInString = itemActionsString.toSet().difference(actionsString.toSet()); final Set missingInString = actionsString.toSet().difference(itemActionsString.toSet()); - if (missingInString.isEmpty && ignoredActions != null && unexpected.every(ignoredActions!.contains)) { + if (missingInString.isEmpty && unexpectedInString.isEmpty) { return true; } return _failWithMessage('Expected actions: $actionsString\nActual actions: $itemActionsString\nUnexpected: $unexpectedInString\nMissing: $missingInString', matchState); diff --git a/dev/integration_tests/android_semantics_testing/pubspec.yaml b/dev/integration_tests/android_semantics_testing/pubspec.yaml index a18ab530d870b..3957ea0cb34ec 100644 --- a/dev/integration_tests/android_semantics_testing/pubspec.yaml +++ b/dev/integration_tests/android_semantics_testing/pubspec.yaml @@ -6,7 +6,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_driver: + integration_test: sdk: flutter flutter_test: sdk: flutter diff --git a/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart b/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart deleted file mode 100644 index 01e536ef14783..0000000000000 --- a/dev/integration_tests/android_semantics_testing/test_driver/main_test.dart +++ /dev/null @@ -1,702 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:android_semantics_testing/android_semantics_testing.dart'; -import 'package:android_semantics_testing/test_constants.dart'; -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:path/path.dart' as path; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart' hide isInstanceOf; - -// The accessibility focus actions are added when a semantics node receives or -// lose accessibility focus. This test ignores these actions since it is hard to -// predict which node has the accessibility focus after a screen changes. -const List ignoredAccessibilityFocusActions = [ - AndroidSemanticsAction.accessibilityFocus, - AndroidSemanticsAction.clearAccessibilityFocus, -]; - -String adbPath() { - final String? androidHome = io.Platform.environment['ANDROID_HOME'] ?? io.Platform.environment['ANDROID_SDK_ROOT']; - if (androidHome == null) { - return 'adb'; - } else { - return path.join(androidHome, 'platform-tools', 'adb'); - } -} - -void main() { - group('AccessibilityBridge', () { - late FlutterDriver driver; - Future getSemantics(SerializableFinder finder) async { - final int id = await driver.getSemanticsId(finder); - final String data = await driver.requestData('getSemanticsNode#$id'); - return AndroidSemanticsNode.deserialize(data); - } - - // The version of TalkBack running on the device. - Version? talkbackVersion; - - Future getTalkbackVersion() async { - final io.ProcessResult result = await io.Process.run(adbPath(), const [ - 'shell', - 'dumpsys', - 'package', - 'com.google.android.marvin.talkback', - ]); - if (result.exitCode != 0) { - throw Exception('Failed to get TalkBack version: ${result.stdout as String}\n${result.stderr as String}'); - } - final List lines = (result.stdout as String).split('\n'); - String? version; - for (final String line in lines) { - if (line.contains('versionName')) { - version = line.replaceAll(RegExp(r'\s*versionName='), ''); - break; - } - } - if (version == null) { - throw Exception('Unable to determine TalkBack version.'); - } - - // Android doesn't quite use semver, so convert the version string to semver form. - final RegExp startVersion = RegExp(r'(?\d+)\.(?\d+)\.(?\d+)(\.(?\d+))?'); - final RegExpMatch? match = startVersion.firstMatch(version); - if (match == null) { - return Version(0, 0, 0); - } - return Version( - int.parse(match.namedGroup('major')!), - int.parse(match.namedGroup('minor')!), - int.parse(match.namedGroup('patch')!), - build: match.namedGroup('build'), - ); - } - - setUpAll(() async { - driver = await FlutterDriver.connect(); - talkbackVersion ??= await getTalkbackVersion(); - print('TalkBack version is $talkbackVersion'); - - // Say the magic words.. - final io.Process run = await io.Process.start(adbPath(), const [ - 'shell', - 'settings', - 'put', - 'secure', - 'enabled_accessibility_services', - 'com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService', - ]); - await run.exitCode; - }); - - tearDownAll(() async { - // ... And turn it off again - final io.Process run = await io.Process.start(adbPath(), const [ - 'shell', - 'settings', - 'put', - 'secure', - 'enabled_accessibility_services', - 'null', - ]); - await run.exitCode; - driver.close(); - }); - - group('TextField', () { - setUpAll(() async { - await driver.tap(find.text(textFieldRoute)); - // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28 - await Future.delayed(const Duration(milliseconds: 500)); - - // The text selection menu and related semantics vary depending on if - // the clipboard contents are pasteable. Copy some text into the - // clipboard to make sure these tests always run with pasteable content - // in the clipboard. - // Ideally this should test the case where there is nothing on the - // clipboard as well, but there is no reliable way to clear the - // clipboard on Android devices. - await driver.requestData('setClipboard#Hello World'); - await Future.delayed(const Duration(milliseconds: 500)); - }); - - test('TextField has correct Android semantics', () async { - final SerializableFinder normalTextField = find.descendant( - of: find.byValueKey(normalTextFieldKeyValue), - matching: find.byType('Semantics'), - firstMatchOnly: true, - ); - expect( - await getSemantics(normalTextField), - hasAndroidSemantics( - className: AndroidClassName.editText, - isEditable: true, - isFocusable: true, - isFocused: false, - isPassword: false, - actions: [ - AndroidSemanticsAction.click, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - - await driver.tap(normalTextField); - // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28 - await Future.delayed(const Duration(milliseconds: 500)); - - expect( - await getSemantics(normalTextField), - hasAndroidSemantics( - className: AndroidClassName.editText, - isFocusable: true, - isFocused: true, - isEditable: true, - isPassword: false, - actions: [ - AndroidSemanticsAction.click, - AndroidSemanticsAction.copy, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.setText, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - - await driver.enterText('hello world'); - // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28 - await Future.delayed(const Duration(milliseconds: 500)); - - expect( - await getSemantics(normalTextField), - hasAndroidSemantics( - text: 'hello world', - className: AndroidClassName.editText, - isFocusable: true, - isFocused: true, - isEditable: true, - isPassword: false, - actions: [ - AndroidSemanticsAction.click, - AndroidSemanticsAction.copy, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.setText, - AndroidSemanticsAction.previousAtMovementGranularity, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - }, timeout: Timeout.none); - - test('password TextField has correct Android semantics', () async { - final SerializableFinder passwordTextField = find.descendant( - of: find.byValueKey(passwordTextFieldKeyValue), - matching: find.byType('Semantics'), - firstMatchOnly: true, - ); - expect( - await getSemantics(passwordTextField), - hasAndroidSemantics( - className: AndroidClassName.editText, - isEditable: true, - isFocusable: true, - isFocused: false, - isPassword: true, - actions: [ - AndroidSemanticsAction.click, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - - await driver.tap(passwordTextField); - // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28 - await Future.delayed(const Duration(milliseconds: 500)); - - expect( - await getSemantics(passwordTextField), - hasAndroidSemantics( - className: AndroidClassName.editText, - isFocusable: true, - isFocused: true, - isEditable: true, - isPassword: true, - actions: [ - AndroidSemanticsAction.click, - AndroidSemanticsAction.copy, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.setText, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - - await driver.enterText('hello world'); - // Delay for TalkBack to update focus as of November 2019 with Pixel 3 and Android API 28 - await Future.delayed(const Duration(milliseconds: 500)); - - expect( - await getSemantics(passwordTextField), - hasAndroidSemantics( - text: '\u{2022}' * ('hello world'.length), - className: AndroidClassName.editText, - isFocusable: true, - isFocused: true, - isEditable: true, - isPassword: true, - actions: [ - AndroidSemanticsAction.click, - AndroidSemanticsAction.copy, - AndroidSemanticsAction.setSelection, - AndroidSemanticsAction.setText, - AndroidSemanticsAction.previousAtMovementGranularity, - ], - // We can't predict the a11y focus when the screen changes. - ignoredActions: ignoredAccessibilityFocusActions, - ), - ); - }, timeout: Timeout.none); - - tearDownAll(() async { - await driver.tap(find.byValueKey('back')); - }); - }); - - group('SelectionControls', () { - setUpAll(() async { - await driver.tap(find.text(selectionControlsRoute)); - }); - - test('Checkbox has correct Android semantics', () async { - Future getCheckboxSemantics(String key) async { - return getSemantics(find.byValueKey(key)); - } - expect( - await getCheckboxSemantics(checkboxKeyValue), - hasAndroidSemantics( - className: AndroidClassName.checkBox, - isChecked: false, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(checkboxKeyValue)); - - expect( - await getCheckboxSemantics(checkboxKeyValue), - hasAndroidSemantics( - className: AndroidClassName.checkBox, - isChecked: true, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - expect( - await getCheckboxSemantics(disabledCheckboxKeyValue), - hasAndroidSemantics( - className: AndroidClassName.checkBox, - isCheckable: true, - isEnabled: false, - ignoredActions: ignoredAccessibilityFocusActions, - actions: const [], - ), - ); - }, timeout: Timeout.none); - test('Radio has correct Android semantics', () async { - Future getRadioSemantics(String key) async { - return getSemantics(find.byValueKey(key)); - } - expect( - await getRadioSemantics(radio2KeyValue), - hasAndroidSemantics( - className: AndroidClassName.radio, - isChecked: false, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(radio2KeyValue)); - - expect( - await getRadioSemantics(radio2KeyValue), - hasAndroidSemantics( - className: AndroidClassName.radio, - isChecked: true, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - }, timeout: Timeout.none); - test('Switch has correct Android semantics', () async { - Future getSwitchSemantics(String key) async { - return getSemantics(find.byValueKey(key)); - } - expect( - await getSwitchSemantics(switchKeyValue), - hasAndroidSemantics( - className: AndroidClassName.toggleSwitch, - isChecked: false, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(switchKeyValue)); - - expect( - await getSwitchSemantics(switchKeyValue), - hasAndroidSemantics( - className: AndroidClassName.toggleSwitch, - isChecked: true, - isCheckable: true, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - }, timeout: Timeout.none); - - // Regression test for https://github.com/flutter/flutter/issues/20820. - test('Switch can be labeled', () async { - Future getSwitchSemantics(String key) async { - return getSemantics(find.byValueKey(key)); - } - expect( - await getSwitchSemantics(labeledSwitchKeyValue), - hasAndroidSemantics( - className: AndroidClassName.toggleSwitch, - isChecked: false, - isCheckable: true, - isEnabled: true, - isFocusable: true, - contentDescription: switchLabel, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - }, timeout: Timeout.none); - - tearDownAll(() async { - await driver.tap(find.byValueKey('back')); - }); - }); - - group('Popup Controls', () { - setUpAll(() async { - await driver.tap(find.text(popupControlsRoute)); - }); - - test('Popup Menu has correct Android semantics', () async { - expect( - await getSemantics(find.byValueKey(popupButtonKeyValue)), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(popupButtonKeyValue)); - try { - // We have to wait wall time here because we're waiting for TalkBack to - // catch up. - await Future.delayed(const Duration(milliseconds: 1500)); - - for (final String item in popupItems) { - expect( - await getSemantics(find.byValueKey('$popupKeyValue.$item')), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Popup $item doesn't have the right semantics"); - } - await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}')); - - // Pop up the menu again, to verify that TalkBack gets the right answer - // more than just the first time. - await driver.tap(find.byValueKey(popupButtonKeyValue)); - await Future.delayed(const Duration(milliseconds: 1500)); - - for (final String item in popupItems) { - expect( - await getSemantics(find.byValueKey('$popupKeyValue.$item')), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Popup $item doesn't have the right semantics the second time"); - } - } finally { - await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}')); - } - }, timeout: Timeout.none); - - test('Dropdown Menu has correct Android semantics', () async { - expect( - await getSemantics(find.byValueKey(dropdownButtonKeyValue)), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(dropdownButtonKeyValue)); - try { - await Future.delayed(const Duration(milliseconds: 1500)); - - for (final String item in popupItems) { - // There are two copies of each item, so we want to find the version - // that is in the overlay, not the one in the dropdown. - expect( - await getSemantics(find.descendant( - of: find.byType('Scrollable'), - matching: find.byValueKey('$dropdownKeyValue.$item'), - )), - hasAndroidSemantics( - className: AndroidClassName.view, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Dropdown $item doesn't have the right semantics"); - } - await driver.tap( - find.descendant( - of: find.byType('Scrollable'), - matching: find.byValueKey('$dropdownKeyValue.${popupItems.first}'), - ), - ); - - // Pop up the dropdown again, to verify that TalkBack gets the right answer - // more than just the first time. - await driver.tap(find.byValueKey(dropdownButtonKeyValue)); - await Future.delayed(const Duration(milliseconds: 1500)); - - for (final String item in popupItems) { - // There are two copies of each item, so we want to find the version - // that is in the overlay, not the one in the dropdown. - expect( - await getSemantics(find.descendant( - of: find.byType('Scrollable'), - matching: find.byValueKey('$dropdownKeyValue.$item'), - )), - hasAndroidSemantics( - className: AndroidClassName.view, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Dropdown $item doesn't have the right semantics the second time."); - } - } finally { - await driver.tap( - find.descendant( - of: find.byType('Scrollable'), - matching: find.byValueKey('$dropdownKeyValue.${popupItems.first}'), - ), - ); - } - }, timeout: Timeout.none); - - test('Modal alert dialog has correct Android semantics', () async { - expect( - await getSemantics(find.byValueKey(alertButtonKeyValue)), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - ); - - await driver.tap(find.byValueKey(alertButtonKeyValue)); - try { - await Future.delayed(const Duration(milliseconds: 1500)); - - expect( - await getSemantics(find.byValueKey('$alertKeyValue.OK')), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Alert OK button doesn't have the right semantics"); - - for (final String item in ['Title', 'Body1', 'Body2']) { - expect( - await getSemantics(find.byValueKey('$alertKeyValue.$item')), - hasAndroidSemantics( - className: AndroidClassName.view, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [], - ), - reason: "Alert $item button doesn't have the right semantics"); - } - - await driver.tap(find.byValueKey('$alertKeyValue.OK')); - - // Pop up the alert again, to verify that TalkBack gets the right answer - // more than just the first time. - await driver.tap(find.byValueKey(alertButtonKeyValue)); - await Future.delayed(const Duration(milliseconds: 1500)); - - expect( - await getSemantics(find.byValueKey('$alertKeyValue.OK')), - hasAndroidSemantics( - className: AndroidClassName.button, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [ - AndroidSemanticsAction.click, - ], - ), - reason: "Alert OK button doesn't have the right semantics"); - - for (final String item in ['Title', 'Body1', 'Body2']) { - expect( - await getSemantics(find.byValueKey('$alertKeyValue.$item')), - hasAndroidSemantics( - className: AndroidClassName.view, - isChecked: false, - isCheckable: false, - isEnabled: true, - isFocusable: true, - ignoredActions: ignoredAccessibilityFocusActions, - actions: [], - ), - reason: "Alert $item button doesn't have the right semantics"); - } - } finally { - await driver.tap(find.byValueKey('$alertKeyValue.OK')); - } - }, timeout: Timeout.none); - - tearDownAll(() async { - await Future.delayed(const Duration(milliseconds: 500)); - await driver.tap(find.byValueKey('back')); - }); - }); - - group('Headings', () { - setUpAll(() async { - await driver.tap(find.text(headingsRoute)); - }); - - test('AppBar title has correct Android heading semantics', () async { - expect( - await getSemantics(find.byValueKey(appBarTitleKeyValue)), - hasAndroidSemantics(isHeading: true), - ); - }, timeout: Timeout.none); - - test('body text does not have Android heading semantics', () async { - expect( - await getSemantics(find.byValueKey(bodyTextKeyValue)), - hasAndroidSemantics(isHeading: false), - ); - }, timeout: Timeout.none); - - tearDownAll(() async { - await driver.tap(find.byValueKey('back')); - }); - }); - - }); -} diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index f041d1f865f88..7177a5fc06452 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -1152,6 +1152,9 @@ class _BottomSheetSuspendedCurve extends ParametricCurve { /// Returns a `Future` that resolves to the value (if any) that was passed to /// [Navigator.pop] when the modal bottom sheet was closed. /// +/// The 'barrierLabel' parameter can be used to set a custom barrierlabel. +/// Will default to modalBarrierDismissLabel of context if not set. +/// /// {@tool dartpad} /// This example demonstrates how to use [showModalBottomSheet] to display a /// bottom sheet that obscures the content behind it when a user taps a button. @@ -1184,6 +1187,7 @@ Future showModalBottomSheet({ required BuildContext context, required WidgetBuilder builder, Color? backgroundColor, + String? barrierLabel, double? elevation, ShapeBorder? shape, Clip? clipBehavior, @@ -1208,7 +1212,7 @@ Future showModalBottomSheet({ builder: builder, capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), isScrollControlled: isScrollControlled, - barrierLabel: localizations.scrimLabel, + barrierLabel: barrierLabel ?? localizations.scrimLabel, barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel), backgroundColor: backgroundColor, elevation: elevation, diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index f3486a13e0362..7c7fa0d7fe386 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -170,11 +170,11 @@ class WordBoundary extends TextBoundary { // single code point that represents a supplementary character. static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) { assert( - TextPainter._isHighSurrogate(highSurrogate), + TextPainter.isHighSurrogate(highSurrogate), 'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.', ); assert( - TextPainter._isLowSurrogate(lowSurrogate), + TextPainter.isLowSurrogate(lowSurrogate), 'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.', ); const int base = 0x010000 - (0xD800 << 10) - 0xDC00; @@ -991,17 +991,34 @@ class TextPainter { canvas.drawParagraph(_paragraph!, offset); } - // Returns true iff the given value is a valid UTF-16 high surrogate. The value - // must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF. - // - // See also: - // * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF - static bool _isHighSurrogate(int value) { + // Returns true if value falls in the valid range of the UTF16 encoding. + static bool _isUTF16(int value) { + return value >= 0x0 && value <= 0xFFFFF; + } + + /// Returns true iff the given value is a valid UTF-16 high (first) surrogate. + /// The value must be a UTF-16 code unit, meaning it must be in the range + /// 0x0000-0xFFFF. + /// + /// See also: + /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF + /// * [isLowSurrogate], which checks the same thing for low (second) + /// surrogates. + static bool isHighSurrogate(int value) { + assert(_isUTF16(value)); return value & 0xFC00 == 0xD800; } - // Whether the given UTF-16 code unit is a low (second) surrogate. - static bool _isLowSurrogate(int value) { + /// Returns true iff the given value is a valid UTF-16 low (second) surrogate. + /// The value must be a UTF-16 code unit, meaning it must be in the range + /// 0x0000-0xFFFF. + /// + /// See also: + /// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF + /// * [isHighSurrogate], which checks the same thing for high (first) + /// surrogates. + static bool isLowSurrogate(int value) { + assert(_isUTF16(value)); return value & 0xFC00 == 0xDC00; } @@ -1021,7 +1038,7 @@ class TextPainter { return null; } // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). - return _isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1; + return isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1; } /// Returns the closest offset before `offset` at which the input cursor can @@ -1032,7 +1049,7 @@ class TextPainter { return null; } // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). - return _isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1; + return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1; } // Unicode value for a zero width joiner character. @@ -1052,7 +1069,7 @@ class TextPainter { const int NEWLINE_CODE_UNIT = 10; // Check for multi-code-unit glyphs such as emojis or zero width joiner. - final bool needsSearch = _isHighSurrogate(prevCodeUnit) || _isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit); + final bool needsSearch = isHighSurrogate(prevCodeUnit) || isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit); int graphemeClusterLength = needsSearch ? 2 : 1; List boxes = []; while (boxes.isEmpty) { @@ -1103,7 +1120,7 @@ class TextPainter { final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1)); // Check for multi-code-unit glyphs such as emojis or zero width joiner - final bool needsSearch = _isHighSurrogate(nextCodeUnit) || _isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit); + final bool needsSearch = isHighSurrogate(nextCodeUnit) || isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit); int graphemeClusterLength = needsSearch ? 2 : 1; List boxes = []; while (boxes.isEmpty) { diff --git a/packages/flutter/lib/src/services/text_boundary.dart b/packages/flutter/lib/src/services/text_boundary.dart index ff6f318bc2327..e7e453e2a3849 100644 --- a/packages/flutter/lib/src/services/text_boundary.dart +++ b/packages/flutter/lib/src/services/text_boundary.dart @@ -31,6 +31,9 @@ abstract class TextBoundary { /// `position`, or null if no boundaries can be found. /// /// The return value, if not null, is usually less than or equal to `position`. + /// + /// The range of the return value is given by the closed interval + /// `[0, string.length]`. int? getLeadingTextBoundaryAt(int position) { if (position < 0) { return null; @@ -39,10 +42,13 @@ abstract class TextBoundary { return start >= 0 ? start : null; } - /// Returns the offset of the closest text boundaries after the given `position`, - /// or null if there is no boundaries can be found after `position`. + /// Returns the offset of the closest text boundary after the given + /// `position`, or null if there is no boundary can be found after `position`. /// /// The return value, if not null, is usually greater than `position`. + /// + /// The range of the return value is given by the closed interval + /// `[0, string.length]`. int? getTrailingTextBoundaryAt(int position) { final int end = getTextBoundaryAt(max(0, position)).end; return end >= 0 ? end : null; diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 51e2faebfe4e7..87bb165979d83 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -4246,7 +4246,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien // --------------------------- Text Editing Actions --------------------------- - TextBoundary _characterBoundary() => widget.obscureText ? _CodeUnitBoundary(_value.text) : CharacterBoundary(_value.text); + TextBoundary _characterBoundary() => widget.obscureText ? _CodePointBoundary(_value.text) : CharacterBoundary(_value.text); TextBoundary _nextWordBoundary() => widget.obscureText ? _documentBoundary() : renderEditable.wordBoundaries.moveByWordBoundary; TextBoundary _linebreak() => widget.obscureText ? _documentBoundary() : LineBoundary(renderEditable); TextBoundary _paragraphBoundary() => ParagraphBoundary(_value.text); @@ -5076,21 +5076,76 @@ class _ScribblePlaceholder extends WidgetSpan { } } -/// A text boundary that uses code units as logical boundaries. +/// A text boundary that uses code points as logical boundaries. /// -/// This text boundary treats every character in input string as an utf-16 code -/// unit. This can be useful when handling text without any grapheme cluster, -/// e.g. password input in [EditableText]. If you are handling text that may -/// include grapheme clusters, consider using [CharacterBoundary]. -class _CodeUnitBoundary extends TextBoundary { - const _CodeUnitBoundary(this._text); +/// A code point represents a single character. This may be smaller than what is +/// represented by a user-perceived character, or grapheme. For example, a +/// single grapheme (in this case a Unicode extended grapheme cluster) like +/// "πŸ‘¨β€πŸ‘©β€πŸ‘¦" consists of five code points: the man emoji, a zero +/// width joiner, the woman emoji, another zero width joiner, and the boy emoji. +/// The [String] has a length of eight because each emoji consists of two code +/// units. +/// +/// Code units are the units by which Dart's String class is measured, which is +/// encoded in UTF-16. +/// +/// See also: +/// +/// * [String.runes], which deals with code points like this class. +/// * [String.characters], which deals with graphemes. +/// * [CharacterBoundary], which is a [TextBoundary] like this class, but whose +/// boundaries are graphemes instead of code points. +class _CodePointBoundary extends TextBoundary { + const _CodePointBoundary(this._text); final String _text; + // Returns true if the given position falls in the center of a surrogate pair. + bool _breaksSurrogatePair(int position) { + assert(position > 0 && position < _text.length && _text.length > 1); + return TextPainter.isHighSurrogate(_text.codeUnitAt(position - 1)) + && TextPainter.isLowSurrogate(_text.codeUnitAt(position)); + } + @override - int getLeadingTextBoundaryAt(int position) => position.clamp(0, _text.length); // ignore_clamp_double_lint + int? getLeadingTextBoundaryAt(int position) { + if (_text.isEmpty || position < 0) { + return null; + } + if (position == 0) { + return 0; + } + if (position >= _text.length) { + return _text.length; + } + if (_text.length <= 1) { + return position; + } + + return _breaksSurrogatePair(position) + ? position - 1 + : position; + } + @override - int getTrailingTextBoundaryAt(int position) => (position + 1).clamp(0, _text.length); // ignore_clamp_double_lint + int? getTrailingTextBoundaryAt(int position) { + if (_text.isEmpty || position >= _text.length) { + return null; + } + if (position < 0) { + return 0; + } + if (position == _text.length - 1) { + return _text.length; + } + if (_text.length <= 1) { + return position; + } + + return _breaksSurrogatePair(position + 1) + ? position + 2 + : position + 1; + } } // ------------------------------- Text Actions ------------------------------- diff --git a/packages/flutter/test/material/bottom_sheet_test.dart b/packages/flutter/test/material/bottom_sheet_test.dart index 8a002286d7d1f..3789255283a37 100644 --- a/packages/flutter/test/material/bottom_sheet_test.dart +++ b/packages/flutter/test/material/bottom_sheet_test.dart @@ -1999,6 +1999,58 @@ void main() { }); }); + + group('showModalBottomSheet modalBarrierDismissLabel', () { + testWidgets('Verify that modalBarrierDismissLabel is used if provided', + (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + const String customLabel = 'custom label'; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: const Center(child: Text('body')), + ), + )); + + showModalBottomSheet( + barrierLabel: 'custom label', + context: scaffoldKey.currentContext!, + builder: (BuildContext context) { + return const Text('BottomSheet'); + }, + ); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + final ModalBarrier modalBarrier = + tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.semanticsLabel, customLabel); + }); + + testWidgets('Verify that modalBarrierDismissLabel from context is used if barrierLabel is not provided', + (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: scaffoldKey, + body: const Center(child: Text('body')), + ), + )); + + showModalBottomSheet( + context: scaffoldKey.currentContext!, + builder: (BuildContext context) { + return const Text('BottomSheet'); + }, + ); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + final ModalBarrier modalBarrier = + tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.semanticsLabel, MaterialLocalizations.of(scaffoldKey.currentContext!).scrimLabel); + }); + }); } class _TestPage extends StatelessWidget { diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart index 9c7eb0bdee478..33f14101523b0 100644 --- a/packages/flutter/test/painting/text_painter_test.dart +++ b/packages/flutter/test/painting/text_painter_test.dart @@ -368,15 +368,14 @@ void main() { test('TextPainter error test', () { final TextPainter painter = TextPainter(textDirection: TextDirection.ltr); - Object? e; - try { - painter.paint(MockCanvas(), Offset.zero); - } catch (exception) { - e = exception; - } + expect( - e.toString(), - contains('TextPainter.paint called when text geometry was not yet calculated'), + () => painter.paint(MockCanvas(), Offset.zero), + throwsA(isA().having( + (StateError error) => error.message, + 'message', + contains('TextPainter.paint called when text geometry was not yet calculated'), + )), ); painter.dispose(); }); @@ -1310,15 +1309,13 @@ void main() { PlaceholderDimensions(size: Size(50, 30), alignment: ui.PlaceholderAlignment.bottom), ]); - Object? e; - try { - painter.paint(MockCanvas(), Offset.zero); - } catch (exception) { - e = exception; - } expect( - e.toString(), - contains('TextPainter.paint called when text geometry was not yet calculated'), + () => painter.paint(MockCanvas(), Offset.zero), + throwsA(isA().having( + (StateError error) => error.message, + 'message', + contains('TextPainter.paint called when text geometry was not yet calculated'), + )), ); painter.dispose(); }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 @@ -1348,16 +1345,14 @@ void main() { PlaceholderDimensions(size: Size(50, 30), alignment: ui.PlaceholderAlignment.bottom), ]); - Object? e; - try { - painter.paint(MockCanvas(), Offset.zero); - } catch (exception) { - e = exception; - } // In tests, paint() will throw an UnimplementedError due to missing drawParagraph method. expect( - e.toString(), - isNot(contains('TextPainter.paint called when text geometry was not yet calculated')), + () => painter.paint(MockCanvas(), Offset.zero), + isNot(throwsA(isA().having( + (StateError error) => error.message, + 'message', + contains('TextPainter.paint called when text geometry was not yet calculated'), + ))), ); painter.dispose(); }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 diff --git a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart index 73c517b6c78a4..b6efb36c221db 100644 --- a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart +++ b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart @@ -444,14 +444,15 @@ void main() { await tester.pumpWidget(buildEditableText(obscured: true)); await sendKeyCombination(tester, const SingleActivator(trigger)); + // Both emojis that were partially selected are deleted entirely. expect( controller.text, - 'πŸ‘¨β€πŸ‘©β€πŸ‘¦πŸ‘¨β€πŸ‘©β€πŸ‘¦', + 'β€πŸ‘©β€πŸ‘¦πŸ‘¨β€πŸ‘©β€πŸ‘¦', ); expect( controller.selection, - const TextSelection.collapsed(offset: 1), + const TextSelection.collapsed(offset: 0), ); }, variant: TargetPlatformVariant.all(excluding: { TargetPlatform.iOS })); }); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 07d37ca52d110..ee94c538cb5e5 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -16091,6 +16091,408 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async expect(state.buildTextSpan().style!.fontWeight, FontWeight.bold); }); + + testWidgets('code points are treated as single characters in obscure mode', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: TextEditingController(), + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + await tester.enterText(find.byType(EditableText), 'πŸ‘¨β€πŸ‘©β€πŸ‘¦'); + await tester.pump(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€πŸ‘¦'); + // πŸ‘¨β€πŸ‘©β€πŸ‘¦| + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 8), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + // πŸ‘¨β€πŸ‘©β€|πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + // πŸ‘¨β€πŸ‘©|β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + // πŸ‘¨β€|πŸ‘©β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 3), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + // πŸ‘¨|β€πŸ‘©β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 2), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + // |πŸ‘¨β€πŸ‘©β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 0), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + // πŸ‘¨|β€πŸ‘©β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 2), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + // πŸ‘¨β€|πŸ‘©β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 3), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + // πŸ‘¨β€πŸ‘©|β€πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + // πŸ‘¨β€πŸ‘©β€|πŸ‘¦ + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + // πŸ‘¨β€πŸ‘©β€πŸ‘¦| + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 8), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€'); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©'); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨β€'); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨'); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, ''); + }, + skip: kIsWeb, // [intended] + ); + + testWidgets('when manually placing the cursor in the middle of a code point', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: TextEditingController(), + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + await tester.enterText(find.byType(EditableText), 'πŸ‘¨β€πŸ‘©β€πŸ‘¦'); + await tester.pump(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€πŸ‘¦'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 8), + ); + + // Place the cursor in the middle of the last code point, which consists of + // two code units. + await tester.tapAt(textOffsetToPosition(tester, 7)); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 7), + ); + + // Using the arrow keys moves out of the code unit. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.tapAt(textOffsetToPosition(tester, 7)); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 7), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 8), + ); + + // Pressing delete doesn't delete only the left code unit, it deletes the + // entire code point (both code units, one to the left and one to the right + // of the cursor). + await tester.tapAt(textOffsetToPosition(tester, 7)); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 7), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + }, + skip: kIsWeb, // [intended] + ); + + testWidgets('when inserting a malformed string', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: TextEditingController(), + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + // This malformed string is the result of removing the final code unit from + // the extended grapheme cluster "πŸ‘¨β€πŸ‘©β€πŸ‘¦", so that the final + // surrogate pair (the "πŸ‘¦" emoji or "\uD83D\uDC66"), only has its high + // surrogate. + await tester.enterText(find.byType(EditableText), 'πŸ‘¨β€πŸ‘©β€\uD83D'); + await tester.pump(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€\uD83D'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 7), + ); + + // The dangling high surrogate is treated as a single rune. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 7), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, 'πŸ‘¨β€πŸ‘©β€'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + }, + skip: kIsWeb, // [intended] + ); + + testWidgets('when inserting a malformed string that is a sequence of dangling high surrogates', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: TextEditingController(), + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + // This string is the high surrogate from the emoji "πŸ‘¦" ("\uD83D\uDC66"), + // repeated. + await tester.enterText(find.byType(EditableText), '\uD83D\uD83D\uD83D\uD83D\uD83D\uD83D'); + await tester.pump(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textEditingValue.text, '\uD83D\uD83D\uD83D\uD83D\uD83D\uD83D'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + // Each dangling high surrogate is treated as a single character. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, '\uD83D\uD83D\uD83D\uD83D\uD83D'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + }, + skip: kIsWeb, // [intended] + ); + + testWidgets('when inserting a malformed string that is a sequence of dangling low surrogates', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: TextEditingController(), + focusNode: focusNode, + obscureText: true, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.tap(find.byType(EditableText)); + // This string is the low surrogate from the emoji "πŸ‘¦" ("\uD83D\uDC66"), + // repeated. + await tester.enterText(find.byType(EditableText), '\uDC66\uDC66\uDC66\uDC66\uDC66\uDC66'); + await tester.pump(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.textEditingValue.text, '\uDC66\uDC66\uDC66\uDC66\uDC66\uDC66'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + // Each dangling high surrogate is treated as a single character. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pump(); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 6), + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + await tester.pump(); + expect(state.textEditingValue.text, '\uDC66\uDC66\uDC66\uDC66\uDC66'); + expect( + state.textEditingValue.selection, + const TextSelection.collapsed(offset: 5), + ); + }, + skip: kIsWeb, // [intended] + ); } class UnsettableController extends TextEditingController { diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 5aa6f2ea1e1b2..8441db2a7df04 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -21,7 +21,7 @@ class UserMessages { // Messages used in FlutterValidator String flutterStatusInfo(String? channel, String? version, String os, String locale) => - 'Channel ${channel ?? 'unknown'}, ${version ?? 'Unknown'}, on $os, locale $locale'; + 'Channel ${channel ?? 'unknown'}, ${version ?? 'unknown version'}, on $os, locale $locale'; String flutterVersion(String version, String channel, String flutterRoot) => 'Flutter version $version on channel $channel at $flutterRoot'; String get flutterUnknownChannel => diff --git a/packages/flutter_tools/lib/src/build_system/targets/macos.dart b/packages/flutter_tools/lib/src/build_system/targets/macos.dart index 8981bb48a0246..366268d59f0e4 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/macos.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/macos.dart @@ -62,6 +62,8 @@ abstract class UnpackMacOS extends Target { basePath, environment.outputDir.path, ]); + + _removeDenylistedFiles(environment.outputDir); if (result.exitCode != 0) { throw Exception( 'Failed to copy framework (exit ${result.exitCode}:\n' @@ -81,6 +83,19 @@ abstract class UnpackMacOS extends Target { _thinFramework(environment, frameworkBinaryPath); } + static const List _copyDenylist = ['entitlements.txt', 'without_entitlements.txt']; + + void _removeDenylistedFiles(Directory directory) { + for (final FileSystemEntity entity in directory.listSync(recursive: true)) { + if (entity is! File) { + continue; + } + if (_copyDenylist.contains(entity.basename)) { + entity.deleteSync(); + } + } + } + void _thinFramework(Environment environment, String frameworkBinaryPath) { final String archs = environment.defines[kDarwinArchs] ?? 'x86_64 arm64'; final List archList = archs.split(' ').toList(); diff --git a/packages/flutter_tools/lib/src/commands/channel.dart b/packages/flutter_tools/lib/src/commands/channel.dart index 80d9bda409030..5a8695ed16438 100644 --- a/packages/flutter_tools/lib/src/commands/channel.dart +++ b/packages/flutter_tools/lib/src/commands/channel.dart @@ -53,13 +53,12 @@ class ChannelCommand extends FlutterCommand { Future _listChannels({ required bool showAll, required bool verbose }) async { // Beware: currentBranch could contain PII. See getBranchName(). - final String currentChannel = globals.flutterVersion.channel; + final String currentChannel = globals.flutterVersion.channel; // limited to known branch names + assert(kOfficialChannels.contains(currentChannel) || kObsoleteBranches.containsKey(currentChannel) || currentChannel == kUserBranch, 'potential PII leak in channel name: "$currentChannel"'); final String currentBranch = globals.flutterVersion.getBranchName(); final Set seenUnofficialChannels = {}; final List rawOutput = []; - showAll = showAll || currentChannel != currentBranch; - globals.printStatus('Flutter channels:'); final int result = await globals.processUtils.stream( ['git', 'branch', '-r'], @@ -74,8 +73,7 @@ class ChannelCommand extends FlutterCommand { throwToolExit('List channels failed: $result$details', exitCode: result); } - final List officialChannels = kOfficialChannels.toList(); - final List availableChannels = List.filled(officialChannels.length, false); + final Set availableChannels = {}; for (final String line in rawOutput) { final List split = line.split('/'); @@ -84,27 +82,25 @@ class ChannelCommand extends FlutterCommand { continue; } final String branch = split[1]; - if (split.length > 1) { - final int index = officialChannels.indexOf(branch); - - if (index != -1) { // Mark all available channels official channels from output - availableChannels[index] = true; - } else if (showAll && !seenUnofficialChannels.contains(branch)) { - // add other branches to seenUnofficialChannels if --all flag is given (to print later) - seenUnofficialChannels.add(branch); - } + if (kOfficialChannels.contains(branch)) { + availableChannels.add(branch); + } else if (showAll) { + seenUnofficialChannels.add(branch); } } + bool currentChannelIsOfficial = false; + // print all available official channels in sorted manner - for (int i = 0; i < officialChannels.length; i++) { + for (final String channel in kOfficialChannels) { // only print non-missing channels - if (availableChannels[i]) { + if (availableChannels.contains(channel)) { String currentIndicator = ' '; - if (officialChannels[i] == currentChannel) { + if (channel == currentChannel) { currentIndicator = '*'; + currentChannelIsOfficial = true; } - globals.printStatus('$currentIndicator ${officialChannels[i]}'); + globals.printStatus('$currentIndicator $channel (${kChannelDescriptions[channel]})'); } } @@ -117,9 +113,12 @@ class ChannelCommand extends FlutterCommand { globals.printStatus(' $branch'); } } + } else if (!currentChannelIsOfficial) { + globals.printStatus('* $currentBranch'); } - if (currentChannel == 'unknown') { + if (!currentChannelIsOfficial) { + assert(currentChannel == kUserBranch, 'Current channel is "$currentChannel", which is not an official branch. (Current branch is "$currentBranch".)'); globals.printStatus(''); globals.printStatus('Currently not on an official channel.'); } diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index c8dba571c5d5a..90c3fa00bbb82 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -407,8 +407,8 @@ abstract class CreateBase extends FlutterCommand { 'iosLanguage': iosLanguage, 'hasIosDevelopmentTeam': iosDevelopmentTeam != null && iosDevelopmentTeam.isNotEmpty, 'iosDevelopmentTeam': iosDevelopmentTeam ?? '', - 'flutterRevision': globals.flutterVersion.frameworkRevision, - 'flutterChannel': globals.flutterVersion.channel, + 'flutterRevision': escapeYamlString(globals.flutterVersion.frameworkRevision), + 'flutterChannel': escapeYamlString(globals.flutterVersion.getBranchName()), // may contain PII 'ios': ios, 'android': android, 'web': web, @@ -571,10 +571,11 @@ abstract class CreateBase extends FlutterCommand { final FlutterProjectMetadata metadata = FlutterProjectMetadata.explicit( file: metadataFile, versionRevision: globals.flutterVersion.frameworkRevision, - versionChannel: globals.flutterVersion.channel, + versionChannel: globals.flutterVersion.getBranchName(), // may contain PII projectType: projectType, migrateConfig: MigrateConfig(), - logger: globals.logger); + logger: globals.logger, + ); metadata.populate( platforms: platformsForMigrateConfig, projectDirectory: directory, diff --git a/packages/flutter_tools/lib/src/commands/downgrade.dart b/packages/flutter_tools/lib/src/commands/downgrade.dart index 8206c8dba63a0..a27482aa87902 100644 --- a/packages/flutter_tools/lib/src/commands/downgrade.dart +++ b/packages/flutter_tools/lib/src/commands/downgrade.dart @@ -47,15 +47,15 @@ class DowngradeCommand extends FlutterCommand { 'working-directory', hide: !verboseHelp, help: 'Override the downgrade working directory. ' - 'This is only intended to enable integration testing of the tool itself.' + 'This is only intended to enable integration testing of the tool itself. ' + 'It allows one to use the flutter tool from one checkout to downgrade a ' + 'different checkout.' ); argParser.addFlag( 'prompt', defaultsTo: true, hide: !verboseHelp, - help: 'Show the downgrade prompt. ' - 'The ability to disable this using "--no-prompt" is only provided for ' - 'integration testing of the tool itself.' + help: 'Show the downgrade prompt.' ); } @@ -99,8 +99,8 @@ class DowngradeCommand extends FlutterCommand { final Channel? channel = getChannelForName(currentChannel); if (channel == null) { throwToolExit( - 'Flutter is not currently on a known channel. Use "flutter channel " ' - 'to switch to an official channel.', + 'Flutter is not currently on a known channel. ' + 'Use "flutter channel" to switch to an official channel. ' ); } final PersistentToolState persistentToolState = _persistentToolState!; @@ -153,13 +153,14 @@ class DowngradeCommand extends FlutterCommand { } on ProcessException catch (error) { throwToolExit( 'Unable to downgrade Flutter: The tool could not update to the version ' - '$humanReadableVersion. This may be due to git not being installed or an ' - 'internal error. Please ensure that git is installed on your computer and ' - 'retry again.\nError: $error.' + '$humanReadableVersion.\n' + 'Error: $error' ); } try { await processUtils.run( + // The `--` bit (because it's followed by nothing) means that we don't actually change + // anything in the working tree, which avoids the need to first go into detached HEAD mode. ['git', 'checkout', currentChannel, '--'], throwOnError: true, workingDirectory: workingDirectory, @@ -167,9 +168,8 @@ class DowngradeCommand extends FlutterCommand { } on ProcessException catch (error) { throwToolExit( 'Unable to downgrade Flutter: The tool could not switch to the channel ' - '$currentChannel. This may be due to git not being installed or an ' - 'internal error. Please ensure that git is installed on your computer ' - 'and retry again.\nError: $error.' + '$currentChannel.\n' + 'Error: $error' ); } await FlutterVersion.resetFlutterVersionFreshnessCheck(); diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 5744df33d781d..3978d0d77654f 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -577,14 +577,14 @@ class FlutterValidator extends DoctorValidator { ValidationMessage _getFlutterVersionMessage(String frameworkVersion, String versionChannel, String flutterRoot) { String flutterVersionMessage = _userMessages.flutterVersion(frameworkVersion, versionChannel, flutterRoot); - // The tool sets the channel as "unknown", if the current branch is on a - // "detached HEAD" state or doesn't have an upstream, and sets the - // frameworkVersion as "0.0.0-unknown" if "git describe" on HEAD doesn't - // produce an expected format to be parsed for the frameworkVersion. - if (versionChannel != 'unknown' && frameworkVersion != '0.0.0-unknown') { + // The tool sets the channel as kUserBranch, if the current branch is on a + // "detached HEAD" state, doesn't have an upstream, or is on a user branch, + // and sets the frameworkVersion as "0.0.0-unknown" if "git describe" on + // HEAD doesn't produce an expected format to be parsed for the frameworkVersion. + if (versionChannel != kUserBranch && frameworkVersion != '0.0.0-unknown') { return ValidationMessage(flutterVersionMessage); } - if (versionChannel == 'unknown') { + if (versionChannel == kUserBranch) { flutterVersionMessage = '$flutterVersionMessage\n${_userMessages.flutterUnknownChannel}'; } if (frameworkVersion == '0.0.0-unknown') { diff --git a/packages/flutter_tools/lib/src/flutter_project_metadata.dart b/packages/flutter_tools/lib/src/flutter_project_metadata.dart index c94693988b2ff..708f79fd5c72a 100644 --- a/packages/flutter_tools/lib/src/flutter_project_metadata.dart +++ b/packages/flutter_tools/lib/src/flutter_project_metadata.dart @@ -8,6 +8,7 @@ import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/utils.dart'; import 'project.dart'; +import 'template.dart'; import 'version.dart'; enum FlutterProjectType implements CliEnum { @@ -172,11 +173,11 @@ class FlutterProjectMetadata { # 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. +# This file should be version controlled and should not be manually edited. version: - revision: $_versionRevision - channel: $_versionChannel + revision: ${escapeYamlString(_versionRevision ?? '')} + channel: ${escapeYamlString(_versionChannel ?? kUserBranch)} project_type: ${projectType == null ? '' : projectType!.cliName} ${migrateConfig.getOutputFileString()}'''; diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index bf3571bec5bfe..0f239393c7299 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart @@ -300,9 +300,6 @@ CustomDevicesConfig get customDevicesConfig => context.get( PreRunValidator get preRunValidator => context.get() ?? const NoOpPreRunValidator(); -// TODO(fujino): Migrate to 'main' https://github.com/flutter/flutter/issues/95041 -const String kDefaultFrameworkChannel = 'master'; - // Used to build RegExp instances which can detect the VM service message. final RegExp kVMServiceMessageRegExp = RegExp(r'The Dart VM service is listening on ((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)'); diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index c476e27a21526..19a8c948a7382 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart @@ -375,3 +375,24 @@ String _escapeKotlinKeywords(String androidIdentifier) { ).toList(); return correctedSegments.join('.'); } + +String escapeYamlString(String value) { + final StringBuffer result = StringBuffer(); + result.write('"'); + for (final int rune in value.runes) { + result.write( + switch (rune) { + 0x00 => r'\0', + 0x09 => r'\t', + 0x0A => r'\n', + 0x0D => r'\r', + 0x22 => r'\"', + 0x5C => r'\\', + < 0x20 => '\\x${rune.toRadixString(16).padLeft(2, "0")}', + _ => String.fromCharCode(rune), + } + ); + } + result.write('"'); + return result.toString(); +} diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index edb8b756fe895..bae196f14b0fe 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -22,6 +22,9 @@ const String _unknownFrameworkVersion = '0.0.0-unknown'; /// See `man gitrevisions` for more information. const String kGitTrackingUpstream = '@{upstream}'; +/// Replacement name when the branch is user-specific. +const String kUserBranch = '[user-branch]'; + /// This maps old branch names to the names of branches that replaced them. /// /// For example, in 2021 we deprecated the "dev" channel and transitioned "dev" @@ -40,12 +43,24 @@ enum Channel { // Beware: Keep order in accordance with stability const Set kOfficialChannels = { - globals.kDefaultFrameworkChannel, + 'master', 'main', 'beta', 'stable', }; +const Map kChannelDescriptions = { + 'master': 'latest development branch, for contributors', + 'main': 'latest development branch, follows master channel', + 'beta': 'updated monthly, recommended for experienced users', + 'stable': 'updated quarterly, for new users and for production app releases', +}; + +const Set kDevelopmentChannels = { + 'master', + 'main', +}; + /// Retrieve a human-readable name for a given [channel]. /// /// Requires [kOfficialChannels] to be correctly ordered. @@ -101,16 +116,7 @@ class FlutterVersion { String? _repositoryUrl; String? get repositoryUrl { - final String _ = channel; - return _repositoryUrl; - } - - String? _channel; - /// The channel is the upstream branch. - /// `master`, `dev`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, ... - String get channel { - String? channel = _channel; - if (channel == null) { + if (_repositoryUrl == null) { final String gitChannel = _runGit( 'git rev-parse --abbrev-ref --symbolic $kGitTrackingUpstream', globals.processUtils, @@ -124,14 +130,16 @@ class FlutterVersion { globals.processUtils, _workingDirectory, ); - channel = gitChannel.substring(slash + 1); - } else if (gitChannel.isEmpty) { - channel = 'unknown'; - } else { - channel = gitChannel; } - _channel = channel; } + return _repositoryUrl; + } + + /// The channel is the current branch if we recognize it, or "[user-branch]" (kUserBranch). + /// `master`, `beta`, `stable`; or old ones, like `alpha`, `hackathon`, `dev`, ... + String get channel { + final String channel = getBranchName(redactUnknownBranches: true); + assert(kOfficialChannels.contains(channel) || kObsoleteBranches.containsKey(channel) || channel == kUserBranch, 'Potential PII leak in channel name: "$channel"'); return channel; } @@ -296,16 +304,16 @@ class FlutterVersion { /// Return the branch name. /// /// If [redactUnknownBranches] is true and the branch is unknown, - /// the branch name will be returned as `'[user-branch]'`. + /// the branch name will be returned as `'[user-branch]'` ([kUserBranch]). String getBranchName({ bool redactUnknownBranches = false }) { _branch ??= () { - final String branch = _runGit('git rev-parse --abbrev-ref HEAD', globals.processUtils); - return branch == 'HEAD' ? channel : branch; + final String branch = _runGit('git symbolic-ref --short HEAD', globals.processUtils, _workingDirectory); + return branch == 'HEAD' ? '' : branch; }(); if (redactUnknownBranches || _branch!.isEmpty) { // Only return the branch names we know about; arbitrary branch names might contain PII. if (!kOfficialChannels.contains(_branch) && !kObsoleteBranches.containsKey(_branch)) { - return '[user-branch]'; + return kUserBranch; } } return _branch!; @@ -619,7 +627,7 @@ String _runSync(List command, { bool lenient = true }) { return ''; } -String _runGit(String command, ProcessUtils processUtils, [String? workingDirectory]) { +String _runGit(String command, ProcessUtils processUtils, String? workingDirectory) { return processUtils.runSync( command.split(' '), workingDirectory: workingDirectory ?? Cache.flutterRoot, @@ -709,8 +717,8 @@ class GitTagVersion { String gitRef = 'HEAD' }) { if (fetchTags) { - final String channel = _runGit('git rev-parse --abbrev-ref HEAD', processUtils, workingDirectory); - if (channel == 'dev' || channel == 'beta' || channel == 'stable') { + final String channel = _runGit('git symbolic-ref --short HEAD', processUtils, workingDirectory); + if (!kDevelopmentChannels.contains(channel) && kOfficialChannels.contains(channel)) { globals.printTrace('Skipping request to fetchTags - on well known channel $channel.'); } else { final String flutterGit = platform.environment['FLUTTER_GIT_URL'] ?? 'https://github.com/flutter/flutter.git'; @@ -918,8 +926,6 @@ class VersionFreshnessValidator { return const Duration(days: 365 ~/ 2); // Six months case 'beta': return const Duration(days: 7 * 8); // Eight weeks - case 'dev': - return const Duration(days: 7 * 4); // Four weeks default: return const Duration(days: 7 * 3); // Three weeks } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart index 90b2e6ee3c0ee..5830675704521 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/downgrade_test.dart @@ -41,7 +41,7 @@ void main() { }); testUsingContext('Downgrade exits on unknown channel', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(branch: 'WestSideStory'); // an unknown branch fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"invalid"}'); final DowngradeCommand command = DowngradeCommand( @@ -58,7 +58,7 @@ void main() { }); testUsingContext('Downgrade exits on no recorded version', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(branch: 'beta'); fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"abcd"}'); final DowngradeCommand command = DowngradeCommand( @@ -86,7 +86,7 @@ void main() { }); testUsingContext('Downgrade exits on unknown recorded version', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"invalid"}'); final DowngradeCommand command = DowngradeCommand( @@ -110,7 +110,7 @@ void main() { }); testUsingContext('Downgrade prompts for user input when terminal is attached - y', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); stdio.hasTerminal = true; fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}'); @@ -131,7 +131,7 @@ void main() { }); testUsingContext('Downgrade prompts for user input when terminal is attached - n', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); stdio.hasTerminal = true; fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}'); @@ -152,7 +152,7 @@ void main() { }); testUsingContext('Downgrade does not prompt when there is no terminal', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); stdio.hasTerminal = false; fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}'); @@ -174,7 +174,7 @@ void main() { }); testUsingContext('Downgrade performs correct git commands', () async { - final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(channel: 'master'); + final FakeFlutterVersion fakeFlutterVersion = FakeFlutterVersion(); stdio.hasTerminal = false; fileSystem.currentDirectory.childFile('.flutter_tool_state') .writeAsStringSync('{"last-active-master-version":"g6b00b5e88"}'); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/upgrade_test.dart index 9d07f82170a87..2a1db24d57a75 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/upgrade_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/upgrade_test.dart @@ -114,7 +114,7 @@ void main() { expect(logger.statusText, contains("Transitioning from 'dev' to 'beta'...")); }, overrides: { FileSystem: () => fileSystem, - FlutterVersion: () => FakeFlutterVersion(channel: 'dev'), + FlutterVersion: () => FakeFlutterVersion(branch: 'dev'), Logger: () => logger, ProcessManager: () => processManager, }); @@ -197,7 +197,7 @@ void main() { ); }, overrides: { FileSystem: () => fileSystem, - FlutterVersion: () => FakeFlutterVersion(channel: 'master', frameworkVersion: startingTag, engineRevision: 'engine'), + FlutterVersion: () => FakeFlutterVersion(frameworkVersion: startingTag, engineRevision: 'engine'), Logger: () => logger, ProcessManager: () => processManager, }); @@ -264,7 +264,7 @@ void main() { ); }, overrides: { FileSystem: () => fileSystem, - FlutterVersion: () => FakeFlutterVersion(channel: 'beta', frameworkVersion: startingTag, engineRevision: 'engine'), + FlutterVersion: () => FakeFlutterVersion(branch: 'beta', frameworkVersion: startingTag, engineRevision: 'engine'), Logger: () => logger, ProcessManager: () => processManager, }); diff --git a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart index 726d0f0425c07..9069a35351744 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/create_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/create_test.dart @@ -78,7 +78,7 @@ void main() { projectDir = tempDir.childDirectory('flutter_project'); fakeFlutterVersion = FakeFlutterVersion( frameworkRevision: frameworkRevision, - channel: frameworkChannel, + branch: frameworkChannel, ); fakeProcessManager = FakeProcessManager.empty(); mockStdio = FakeStdio(); @@ -1269,8 +1269,8 @@ void main() { expectExists(versionPath); final String version = globals.fs.file(globals.fs.path.join(projectDir.path, versionPath)).readAsStringSync(); expect(version, contains('version:')); - expect(version, contains('revision: 12345678')); - expect(version, contains('channel: omega')); + expect(version, contains('revision: "12345678"')); + expect(version, contains('channel: "omega"')); // IntelliJ metadata final String intelliJSdkMetadataPath = globals.fs.path.join('.idea', 'libraries', 'Dart_SDK.xml'); @@ -1349,8 +1349,8 @@ void main() { expectExists(versionPath); final String version = globals.fs.file(globals.fs.path.join(projectDir.path, versionPath)).readAsStringSync(); expect(version, contains('version:')); - expect(version, contains('revision: 12345678')); - expect(version, contains('channel: omega')); + expect(version, contains('revision: "12345678"')); + expect(version, contains('channel: "omega"')); // IntelliJ metadata final String intelliJSdkMetadataPath = globals.fs.path.join('.idea', 'libraries', 'Dart_SDK.xml'); diff --git a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart index ae507da6b5235..bddf42d0b3623 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/upgrade_test.dart @@ -46,7 +46,7 @@ void main() { }); testUsingContext('throws on unknown tag, official branch, noforce', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta'); const String upstreamRevision = ''; final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: upstreamRevision); fakeCommandRunner.remoteVersion = latestVersion; @@ -66,7 +66,7 @@ void main() { }); testUsingContext('throws tool exit with uncommitted changes', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta'); const String upstreamRevision = ''; final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: upstreamRevision); fakeCommandRunner.remoteVersion = latestVersion; @@ -89,7 +89,7 @@ void main() { testUsingContext("Doesn't continue on known tag, beta branch, no force, already up-to-date", () async { const String revision = 'abc123'; final FakeFlutterVersion latestVersion = FakeFlutterVersion(frameworkRevision: revision); - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta', frameworkRevision: revision); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta', frameworkRevision: revision); fakeCommandRunner.alreadyUpToDate = true; fakeCommandRunner.remoteVersion = latestVersion; @@ -116,7 +116,7 @@ void main() { const String upstreamVersion = '4.5.6'; final FakeFlutterVersion flutterVersion = FakeFlutterVersion( - channel: 'beta', + branch: 'beta', frameworkRevision: revision, frameworkRevisionShort: revision, frameworkVersion: version, @@ -287,7 +287,7 @@ void main() { const String upstreamVersion = '4.5.6'; final FakeFlutterVersion flutterVersion = FakeFlutterVersion( - channel: 'beta', + branch: 'beta', frameworkRevision: revision, frameworkVersion: version, ); @@ -348,7 +348,7 @@ void main() { testUsingContext('does not throw on unknown tag, official branch, force', () async { fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234'); - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta'); final Future result = fakeCommandRunner.runCommand( force: true, @@ -366,7 +366,7 @@ void main() { }); testUsingContext('does not throw tool exit with uncommitted changes and force', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta'); fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234'); fakeCommandRunner.willHaveUncommittedChanges = true; @@ -386,7 +386,7 @@ void main() { }); testUsingContext("Doesn't throw on known tag, beta branch, no force", () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: 'beta'); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: 'beta'); fakeCommandRunner.remoteVersion = FakeFlutterVersion(frameworkRevision: '1234'); final Future result = fakeCommandRunner.runCommand( diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index 176b76b9ad140..2d705301e0fb9 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -106,6 +106,51 @@ void main() { ProcessManager: () => processManager, }); + testUsingContext('deletes entitlements.txt and without_entitlements.txt files after copying', () async { + binary.createSync(recursive: true); + final File entitlements = environment.outputDir.childFile('entitlements.txt'); + final File withoutEntitlements = environment.outputDir.childFile('without_entitlements.txt'); + final File nestedEntitlements = environment + .outputDir + .childDirectory('first_level') + .childDirectory('second_level') + .childFile('entitlements.txt') + ..createSync(recursive: true); + + processManager.addCommands([ + FakeCommand( + command: [ + 'rsync', + '-av', + '--delete', + '--filter', + '- .DS_Store/', + // source + 'Artifact.flutterMacOSFramework.debug', + // destination + environment.outputDir.path, + ], + onRun: () { + entitlements.writeAsStringSync('foo'); + withoutEntitlements.writeAsStringSync('bar'); + nestedEntitlements.writeAsStringSync('somefile.bin'); + }, + ), + lipoInfoNonFatCommand, + lipoVerifyX86_64Command, + ]); + + await const DebugUnpackMacOS().build(environment); + expect(entitlements.existsSync(), isFalse); + expect(withoutEntitlements.existsSync(), isFalse); + expect(nestedEntitlements.existsSync(), isFalse); + + expect(processManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }); + testUsingContext('thinning fails when framework missing', () async { processManager.addCommand(copyFrameworkCommand); await expectLater( diff --git a/packages/flutter_tools/test/general.shard/channel_test.dart b/packages/flutter_tools/test/general.shard/channel_test.dart index 5187fa76a3d40..f75c92fff2315 100644 --- a/packages/flutter_tools/test/general.shard/channel_test.dart +++ b/packages/flutter_tools/test/general.shard/channel_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/version.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/fake_process_manager.dart'; +import '../src/fakes.dart' show FakeFlutterVersion; import '../src/test_flutter_command_runner.dart'; void main() { @@ -29,7 +30,16 @@ void main() { Future simpleChannelTest(List args) async { fakeProcessManager.addCommands(const [ - FakeCommand(command: ['git', 'branch', '-r'], stdout: ' branch-1\n branch-2'), + FakeCommand( + command: ['git', 'branch', '-r'], + stdout: + ' origin/branch-1\n' + ' origin/branch-2\n' + ' origin/master\n' + ' origin/main\n' + ' origin/stable\n' + ' origin/beta', + ), ]); final ChannelCommand command = ChannelCommand(); final CommandRunner runner = createTestCommandRunner(command); @@ -75,11 +85,13 @@ void main() { await runner.run(['channel']); expect(fakeProcessManager, hasNoRemainingExpectations); expect(testLogger.errorText, hasLength(0)); - // format the status text for a simpler assertion. - final Iterable rows = testLogger.statusText - .split('\n') - .map((String line) => line.substring(2)); // remove '* ' or ' ' from output - expect(rows, containsAllInOrder(kOfficialChannels)); + expect(testLogger.statusText, + 'Flutter channels:\n' + '* master (latest development branch, for contributors)\n' + ' main (latest development branch, follows master channel)\n' + ' beta (updated monthly, recommended for experienced users)\n' + ' stable (updated quarterly, for new users and for production app releases)\n', + ); // clear buffer for next process testLogger.clear(); @@ -99,13 +111,14 @@ void main() { await runner.run(['channel']); expect(fakeProcessManager, hasNoRemainingExpectations); - expect(rows, containsAllInOrder(kOfficialChannels)); expect(testLogger.errorText, hasLength(0)); - // format the status text for a simpler assertion. - final Iterable rows2 = testLogger.statusText - .split('\n') - .map((String line) => line.substring(2)); // remove '* ' or ' ' from output - expect(rows2, containsAllInOrder(kOfficialChannels)); + expect(testLogger.statusText, + 'Flutter channels:\n' + '* master (latest development branch, for contributors)\n' + ' main (latest development branch, follows master channel)\n' + ' beta (updated monthly, recommended for experienced users)\n' + ' stable (updated quarterly, for new users and for production app releases)\n', + ); // clear buffer for next process testLogger.clear(); @@ -114,10 +127,11 @@ void main() { fakeProcessManager.addCommand( const FakeCommand( command: ['git', 'branch', '-r'], - stdout: 'origin/beta\n' + stdout: 'origin/master\n' 'origin/dependabot/bundler\n' 'origin/v1.4.5-hotfixes\n' - 'origin/stable\n', + 'origin/stable\n' + 'origin/beta\n', ), ); @@ -158,18 +172,45 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); expect(testLogger.errorText, hasLength(0)); + expect(testLogger.statusText, + 'Flutter channels:\n' + '* beta (updated monthly, recommended for experienced users)\n' + ' stable (updated quarterly, for new users and for production app releases)\n' + ); + }, overrides: { + ProcessManager: () => fakeProcessManager, + FileSystem: () => MemoryFileSystem.test(), + FlutterVersion: () => FakeFlutterVersion(branch: 'beta'), + }); - // format the status text for a simpler assertion. - final Iterable rows = testLogger.statusText - .split('\n') - .map((String line) => line.trim()) - .where((String line) => line.isNotEmpty) - .skip(1); // remove `Flutter channels:` line + testUsingContext('handles custom branches', () async { + fakeProcessManager.addCommand( + const FakeCommand( + command: ['git', 'branch', '-r'], + stdout: 'origin/beta\n' + 'origin/stable\n' + 'origin/foo', + ), + ); + + final ChannelCommand command = ChannelCommand(); + final CommandRunner runner = createTestCommandRunner(command); + await runner.run(['channel']); - expect(rows, ['beta', 'stable', 'Currently not on an official channel.']); + expect(fakeProcessManager, hasNoRemainingExpectations); + expect(testLogger.errorText, hasLength(0)); + expect(testLogger.statusText, + 'Flutter channels:\n' + ' beta (updated monthly, recommended for experienced users)\n' + ' stable (updated quarterly, for new users and for production app releases)\n' + '* foo\n' + '\n' + 'Currently not on an official channel.\n', + ); }, overrides: { ProcessManager: () => fakeProcessManager, FileSystem: () => MemoryFileSystem.test(), + FlutterVersion: () => FakeFlutterVersion(branch: 'foo'), }); testUsingContext('removes duplicates', () async { @@ -189,18 +230,15 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); expect(testLogger.errorText, hasLength(0)); - - // format the status text for a simpler assertion. - final Iterable rows = testLogger.statusText - .split('\n') - .map((String line) => line.trim()) - .where((String line) => line.isNotEmpty) - .skip(1); // remove `Flutter channels:` line - - expect(rows, ['beta', 'stable', 'Currently not on an official channel.']); + expect(testLogger.statusText, + 'Flutter channels:\n' + '* beta (updated monthly, recommended for experienced users)\n' + ' stable (updated quarterly, for new users and for production app releases)\n' + ); }, overrides: { ProcessManager: () => fakeProcessManager, FileSystem: () => MemoryFileSystem.test(), + FlutterVersion: () => FakeFlutterVersion(branch: 'beta'), }); testUsingContext('can switch channels', () async { diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index df9d60375632a..f15220e90b542 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -33,7 +33,7 @@ void main() { FeatureFlags createFlags(String channel) { return FlutterFeatureFlags( - flutterVersion: FakeFlutterVersion(channel: channel), + flutterVersion: FakeFlutterVersion(branch: channel), config: testConfig, platform: platform, ); diff --git a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart index 6a20dba80a8c7..f2cf86389a919 100644 --- a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart @@ -34,7 +34,7 @@ void main() { 'downloaded and exits with code 1', () async { final FakeFlutterVersion flutterVersion = FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', ); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Artifacts artifacts = Artifacts.test(); @@ -78,7 +78,7 @@ void main() { testWithoutContext('FlutterValidator shows an error message if Rosetta is needed', () async { final FakeFlutterVersion flutterVersion = FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', ); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final Artifacts artifacts = Artifacts.test(); @@ -121,7 +121,7 @@ void main() { testWithoutContext('FlutterValidator does not run gen_snapshot binary check if it is not already downloaded', () async { final FakeFlutterVersion flutterVersion = FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', ); final FlutterValidator flutterValidator = FlutterValidator( platform: FakePlatform( @@ -174,7 +174,7 @@ void main() { testWithoutContext('FlutterValidator shows mirrors on pub and flutter cloud storage', () async { final FakeFlutterVersion flutterVersion = FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', ); final Platform platform = FakePlatform( operatingSystem: 'windows', @@ -218,7 +218,7 @@ void main() { ), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -247,8 +247,8 @@ void main() { final FlutterValidator flutterValidator = FlutterValidator( platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( + branch: 'unknown', frameworkVersion: '1.0.0', - // channel is unknown by default ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -261,10 +261,10 @@ void main() { expect(await flutterValidator.validate(), _matchDoctorValidation( validationType: ValidationType.partial, - statusInfo: 'Channel unknown, 1.0.0, on Linux, locale en_US.UTF-8', + statusInfo: 'Channel [user-branch], 1.0.0, on Linux, locale en_US.UTF-8', messages: containsAll([ const ValidationMessage.hint( - 'Flutter version 1.0.0 on channel unknown at /sdk/flutter\n' + 'Flutter version 1.0.0 on channel [user-branch] at /sdk/flutter\n' 'Currently on an unknown channel. Run `flutter channel` to switch to an official channel.\n' "If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/docs/get-started/install." ), @@ -281,7 +281,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '0.0.0-unknown', - channel: 'beta', + branch: 'beta', ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -315,7 +315,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -338,7 +338,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', repositoryUrl: 'https://githubmirror.com/flutter.git' ), devToolsVersion: () => '2.8.0', @@ -372,7 +372,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta', + branch: 'beta', repositoryUrl: null, ), devToolsVersion: () => '2.8.0', @@ -406,7 +406,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -435,7 +435,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -472,7 +472,7 @@ void main() { platform: FakePlatform(operatingSystem: 'windows', localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -513,7 +513,7 @@ void main() { platform: FakePlatform(operatingSystem: 'windows', localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -546,7 +546,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), @@ -580,7 +580,7 @@ void main() { platform: FakePlatform(localeName: 'en_US.UTF-8'), flutterVersion: () => FakeFlutterVersion( frameworkVersion: '1.0.0', - channel: 'beta' + branch: 'beta' ), devToolsVersion: () => '2.8.0', userMessages: UserMessages(), diff --git a/packages/flutter_tools/test/general.shard/template_test.dart b/packages/flutter_tools/test/general.shard/template_test.dart index 368a5dc1f0ae9..4f3cc55334d66 100644 --- a/packages/flutter_tools/test/general.shard/template_test.dart +++ b/packages/flutter_tools/test/general.shard/template_test.dart @@ -194,6 +194,23 @@ void main() { expect(logger.statusText, isEmpty); }); }); + + testWithoutContext('escapeYamlString', () { + expect(escapeYamlString(''), r'""'); + expect(escapeYamlString('\x00\n\r\t\b'), r'"\0\n\r\t\x08"'); + expect(escapeYamlString('test'), r'"test"'); + expect(escapeYamlString('test\n test'), r'"test\n test"'); + expect(escapeYamlString('\x00\x01\x02\x0c\x19\xab'), r'"\0\x01\x02\x0c\x19Β«"'); + expect(escapeYamlString('"'), r'"\""'); + expect(escapeYamlString(r'\'), r'"\\"'); + expect(escapeYamlString('[user branch]'), r'"[user branch]"'); + expect(escapeYamlString('main'), r'"main"'); + expect(escapeYamlString('TEST_BRANCH'), r'"TEST_BRANCH"'); + expect(escapeYamlString(' '), r'" "'); + expect(escapeYamlString(' \n '), r'" \n "'); + expect(escapeYamlString('""'), r'"\"\""'); + expect(escapeYamlString('"\x01\u{0263A}\u{1F642}'), r'"\"\x01β˜ΊπŸ™‚"'); + }); } class FakeTemplateRenderer extends TemplateRenderer { diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index 861d90a8ef4ad..6fe00447e6c44 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -16,6 +16,7 @@ import 'package:test/fake.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/fake_process_manager.dart'; +import '../src/fakes.dart' show FakeFlutterVersion; final SystemClock _testClock = SystemClock.fixed(DateTime(2015)); final DateTime _stampUpToDate = _testClock.ago(VersionFreshnessValidator.checkAgeConsideredUpToDate ~/ 2); @@ -67,6 +68,10 @@ void main() { command: ['git', 'describe', '--match', '*.*.*', '--long', '--tags', '1234abcd'], stdout: '0.1.2-3-1234abcd', ), + FakeCommand( + command: const ['git', 'symbolic-ref', '--short', 'HEAD'], + stdout: channel, + ), FakeCommand( command: const ['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'], stdout: 'origin/$channel', @@ -94,10 +99,6 @@ void main() { command: const ['git', '-c', 'log.showSignature=false', 'log', 'HEAD', '-n', '1', '--pretty=format:%ad', '--date=iso'], stdout: getChannelUpToDateVersion().toString(), ), - FakeCommand( - command: const ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - stdout: channel, - ), ]); final FlutterVersion flutterVersion = globals.flutterVersion; @@ -129,7 +130,7 @@ void main() { }); testWithoutContext('prints nothing when Flutter installation looks out-of-date but is actually up-to-date', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); final VersionCheckStamp stamp = VersionCheckStamp( lastTimeVersionWasChecked: _stampOutOfDate, @@ -150,7 +151,7 @@ void main() { }); testWithoutContext('does not ping server when version stamp is up-to-date', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); final VersionCheckStamp stamp = VersionCheckStamp( lastTimeVersionWasChecked: _stampUpToDate, @@ -172,7 +173,7 @@ void main() { }); testWithoutContext('does not print warning if printed recently', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); final VersionCheckStamp stamp = VersionCheckStamp( lastTimeVersionWasChecked: _stampUpToDate, @@ -194,7 +195,7 @@ void main() { }); testWithoutContext('pings server when version stamp is missing', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); cache.versionStamp = '{}'; @@ -212,7 +213,7 @@ void main() { }); testWithoutContext('pings server when version stamp is out-of-date', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); final VersionCheckStamp stamp = VersionCheckStamp( lastTimeVersionWasChecked: _stampOutOfDate, @@ -233,7 +234,7 @@ void main() { }); testWithoutContext('does not print warning when unable to connect to server if not out of date', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); cache.versionStamp = '{}'; @@ -250,7 +251,7 @@ void main() { }); testWithoutContext('prints warning when unable to connect to server if really out of date', () async { - final FakeFlutterVersion flutterVersion = FakeFlutterVersion(channel: channel); + final FakeFlutterVersion flutterVersion = FakeFlutterVersion(branch: channel); final BufferLogger logger = BufferLogger.test(); final VersionCheckStamp stamp = VersionCheckStamp( lastTimeVersionWasChecked: _stampOutOfDate, @@ -329,7 +330,7 @@ void main() { if (flutterGitUrl != null) 'FLUTTER_GIT_URL': flutterGitUrl, }); return VersionUpstreamValidator( - version: FakeFlutterVersion(repositoryUrl: versionUpstreamUrl, channel: 'master'), + version: FakeFlutterVersion(repositoryUrl: versionUpstreamUrl), platform: testPlatform, ).run(); } @@ -413,17 +414,13 @@ void main() { stdout: '0.1.2-3-1234abcd', ), const FakeCommand( - command: ['git', 'rev-parse', '--abbrev-ref', '--symbolic', '@{upstream}'], - stdout: 'feature-branch', - ), - const FakeCommand( - command: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + command: ['git', 'symbolic-ref', '--short', 'HEAD'], stdout: 'feature-branch', ), ]); final FlutterVersion flutterVersion = globals.flutterVersion; - expect(flutterVersion.channel, 'feature-branch'); + expect(flutterVersion.channel, '[user-branch]'); expect(flutterVersion.getVersionString(), 'feature-branch/1234abcd'); expect(flutterVersion.getBranchName(), 'feature-branch'); expect(flutterVersion.getVersionString(redactUnknownBranches: true), '[user-branch]/1234abcd'); @@ -455,7 +452,7 @@ void main() { expect(gitTagVersion.devVersion, null); expect(gitTagVersion.devPatch, null); - // Dev channel + // Beta channel gitTagVersion = GitTagVersion.parse('1.2.3-4.5.pre'); expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-4.5.pre'); expect(gitTagVersion.gitTag, '1.2.3-4.5.pre'); @@ -468,7 +465,7 @@ void main() { expect(gitTagVersion.devVersion, null); expect(gitTagVersion.devPatch, null); - // new tag release format, dev channel + // new tag release format, beta channel gitTagVersion = GitTagVersion.parse('1.2.3-4.5.pre-0-g$hash'); expect(gitTagVersion.frameworkVersionFor(hash), '1.2.3-4.5.pre'); expect(gitTagVersion.gitTag, '1.2.3-4.5.pre'); @@ -521,13 +518,13 @@ void main() { expect(gitTagVersion.frameworkVersionFor('abcd1234'), stableTag); }); - testUsingContext('determine favors stable tag over dev tag if both identify HEAD', () { + testUsingContext('determine favors stable tag over beta tag if both identify HEAD', () { const String stableTag = '1.2.3'; final FakeProcessManager fakeProcessManager = FakeProcessManager.list( [ const FakeCommand( command: ['git', 'tag', '--points-at', 'HEAD'], - // This tests the unlikely edge case where a dev release made it to stable without any cherry picks + // This tests the unlikely edge case where a beta release made it to stable without any cherry picks stdout: '1.2.3-6.0.pre\n$stableTag', ), ], @@ -589,11 +586,11 @@ void main() { expect(fakeProcessManager, hasNoRemainingExpectations); }); - testUsingContext('determine does not fetch tags on dev/stable/beta', () { + testUsingContext('determine does not fetch tags on beta', () { final FakeProcessManager fakeProcessManager = FakeProcessManager.list([ const FakeCommand( - command: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - stdout: 'dev', + command: ['git', 'symbolic-ref', '--short', 'HEAD'], + stdout: 'beta', ), const FakeCommand( command: ['git', 'tag', '--points-at', 'HEAD'], @@ -616,7 +613,7 @@ void main() { testUsingContext('determine calls fetch --tags on master', () { final FakeProcessManager fakeProcessManager = FakeProcessManager.list([ const FakeCommand( - command: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + command: ['git', 'symbolic-ref', '--short', 'HEAD'], stdout: 'master', ), const FakeCommand( @@ -643,7 +640,7 @@ void main() { testUsingContext('determine uses overridden git url', () { final FakeProcessManager fakeProcessManager = FakeProcessManager.list([ const FakeCommand( - command: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + command: ['git', 'symbolic-ref', '--short', 'HEAD'], stdout: 'master', ), const FakeCommand( @@ -701,13 +698,3 @@ class FakeCache extends Fake implements Cache { } } } - -class FakeFlutterVersion extends Fake implements FlutterVersion { - FakeFlutterVersion({required this.channel, this.repositoryUrl}); - - @override - final String channel; - - @override - final String? repositoryUrl; -} diff --git a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart index 3b8a49558f5ac..26ac8752cc1cf 100644 --- a/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/downgrade_upgrade_integration_test.dart @@ -11,7 +11,7 @@ import 'package:flutter_tools/src/base/terminal.dart'; import '../src/common.dart'; import 'test_utils.dart'; -const String _kInitialVersion = 'v1.9.1'; +const String _kInitialVersion = '3.0.0'; const String _kBranch = 'beta'; final Stdio stdio = Stdio(); @@ -80,6 +80,7 @@ void main() { printOnFailure('Step 4 - upgrade to the newest $_kBranch'); // This should update the persistent tool state with the sha for HEAD + // This is probably a source of flakes as it mutates system-global state. exitCode = await processUtils.stream([ flutterBin, 'upgrade', @@ -93,7 +94,7 @@ void main() { 'git', 'describe', '--match', - 'v*.*.*', + '*.*.*', '--long', '--tags', ], workingDirectory: testDirectory.path); @@ -114,7 +115,7 @@ void main() { 'git', 'describe', '--match', - 'v*.*.*', + '*.*.*', '--long', '--tags', ], workingDirectory: testDirectory.path); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 7760aea6a1396..30d379d7af505 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -324,7 +324,7 @@ class FakeBotDetector implements BotDetector { class FakeFlutterVersion implements FlutterVersion { FakeFlutterVersion({ - this.channel = 'unknown', + this.branch = 'master', this.dartSdkVersion = '12', this.devToolsVersion = '2.8.0', this.engineRevision = 'abcdefghijklmnopqrstuvwxyz', @@ -338,6 +338,8 @@ class FakeFlutterVersion implements FlutterVersion { this.gitTagVersion = const GitTagVersion.unknown(), }); + final String branch; + bool get didFetchTagsAndUpdate => _didFetchTagsAndUpdate; bool _didFetchTagsAndUpdate = false; @@ -345,7 +347,12 @@ class FakeFlutterVersion implements FlutterVersion { bool _didCheckFlutterVersionFreshness = false; @override - final String channel; + String get channel { + if (kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) { + return branch; + } + return kUserBranch; + } @override final String devToolsVersion; @@ -398,7 +405,10 @@ class FakeFlutterVersion implements FlutterVersion { @override String getBranchName({bool redactUnknownBranches = false}) { - return 'master'; + if (!redactUnknownBranches || kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) { + return branch; + } + return kUserBranch; } @override