mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add android semantics integration test to device lab (#20971)
This commit is contained in:
parent
a4838a2adc
commit
c6e8a513f0
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2018 The Chromium 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 'package:flutter_devicelab/framework/adb.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/integration_tests.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.android;
|
||||||
|
await task(createAndroidSemanticsIntegrationTest());
|
||||||
|
}
|
@ -52,6 +52,13 @@ TaskFunction createEmbeddedAndroidViewsIntegrationTest() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskFunction createAndroidSemanticsIntegrationTest() {
|
||||||
|
return new DriverTest(
|
||||||
|
'${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
|
||||||
|
'lib/main.dart',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class DriverTest {
|
class DriverTest {
|
||||||
|
|
||||||
DriverTest(
|
DriverTest(
|
||||||
|
@ -136,6 +136,13 @@ tasks:
|
|||||||
stage: devicelab
|
stage: devicelab
|
||||||
required_agent_capabilities: ["mac/android"]
|
required_agent_capabilities: ["mac/android"]
|
||||||
|
|
||||||
|
android_semantics_integration_test:
|
||||||
|
description: >
|
||||||
|
Tests that the Android accessibility bridge produces correct semantics.
|
||||||
|
stage: devicelab
|
||||||
|
required_agent_capabilities: ["mac/android"]
|
||||||
|
flaky: true
|
||||||
|
|
||||||
run_release_test:
|
run_release_test:
|
||||||
description: >
|
description: >
|
||||||
Checks that `flutter run --release` does not crash.
|
Checks that `flutter run --release` does not crash.
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withInputStream { stream ->
|
||||||
|
localProperties.load(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||||
|
if (flutterRoot == null) {
|
||||||
|
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 27
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'InvalidPackage'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 16
|
||||||
|
targetSdkVersion 27
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.0.1"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aaptOptions {
|
||||||
|
// TODO(goderbauer): remove when https://github.com/flutter/flutter/issues/8986 is resolved.
|
||||||
|
if(System.getenv("FLUTTER_CI_WIN")) {
|
||||||
|
println "AAPT cruncher disabled when running on Win CI."
|
||||||
|
cruncherEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.yourcompany.platforminteraction">
|
||||||
|
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
flutter needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
|
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||||
|
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||||
|
In most cases you can leave this as-is, but you if you want to provide
|
||||||
|
additional functionality it is fine to subclass or reimplement
|
||||||
|
FlutterApplication and put your custom class here. -->
|
||||||
|
<application android:name="io.flutter.app.FlutterApplication" android:label="Platform Interaction" android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity android:name="com.yourcompany.platforminteraction.MainActivity"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@android:style/Theme.Black.NoTitleBar"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
package com.yourcompany.platforminteraction;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.lang.StringBuilder;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import io.flutter.app.FlutterActivity;
|
||||||
|
import io.flutter.plugin.common.MethodCall;
|
||||||
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||||
|
import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||||
|
import io.flutter.view.FlutterView;
|
||||||
|
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
import android.view.accessibility.AccessibilityNodeProvider;
|
||||||
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
|
|
||||||
|
public class MainActivity extends FlutterActivity {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
GeneratedPluginRegistrant.registerWith(this);
|
||||||
|
new MethodChannel(getFlutterView(), "semantics").setMethodCallHandler(new SemanticsTesterMethodHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
class SemanticsTesterMethodHandler implements MethodCallHandler {
|
||||||
|
Float mScreenDensity = 1.0f;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
|
||||||
|
FlutterView flutterView = getFlutterView();
|
||||||
|
AccessibilityNodeProvider provider = flutterView.getAccessibilityNodeProvider();
|
||||||
|
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||||
|
WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
wm.getDefaultDisplay().getMetrics(displayMetrics);
|
||||||
|
mScreenDensity = displayMetrics.density;
|
||||||
|
if (methodCall.method.equals("getSemanticsNode")) {
|
||||||
|
Map<String, Object> data = methodCall.arguments();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Integer id = (Integer) data.get("id");
|
||||||
|
if (id == null) {
|
||||||
|
result.error("No ID provided", "", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (provider == null) {
|
||||||
|
result.error("Semantics not enabled", "", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AccessibilityNodeInfo node = provider.createAccessibilityNodeInfo(id);
|
||||||
|
result.success(convertSemantics(node, id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.notImplemented();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> convertSemantics(AccessibilityNodeInfo node, int id) {
|
||||||
|
if (node == null)
|
||||||
|
return null;
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
Map<String, Object> flags = new HashMap<>();
|
||||||
|
Map<String, Object> rect = new HashMap<>();
|
||||||
|
result.put("id", id);
|
||||||
|
result.put("text", node.getText());
|
||||||
|
result.put("contentDescription", node.getContentDescription());
|
||||||
|
flags.put("isChecked", node.isChecked());
|
||||||
|
flags.put("isCheckable", node.isCheckable());
|
||||||
|
flags.put("isDismissable", node.isDismissable());
|
||||||
|
flags.put("isEditable", node.isEditable());
|
||||||
|
flags.put("isEnabled", node.isEnabled());
|
||||||
|
flags.put("isFocusable", node.isFocusable());
|
||||||
|
flags.put("isFocused", node.isFocused());
|
||||||
|
flags.put("isPassword", node.isPassword());
|
||||||
|
flags.put("isLongClickable", node.isLongClickable());
|
||||||
|
result.put("flags", flags);
|
||||||
|
Rect nodeRect = new Rect();
|
||||||
|
node.getBoundsInScreen(nodeRect);
|
||||||
|
rect.put("left", nodeRect.left / mScreenDensity);
|
||||||
|
rect.put("top", nodeRect.top/ mScreenDensity);
|
||||||
|
rect.put("right", nodeRect.right / mScreenDensity);
|
||||||
|
rect.put("bottom", nodeRect.bottom/ mScreenDensity);
|
||||||
|
rect.put("width", nodeRect.width());
|
||||||
|
rect.put("height", nodeRect.height());
|
||||||
|
result.put("rect", rect);
|
||||||
|
result.put("className", node.getClassName());
|
||||||
|
result.put("contentDescription", node.getContentDescription());
|
||||||
|
result.put("liveRegion", node.getLiveRegion());
|
||||||
|
List<AccessibilityNodeInfo.AccessibilityAction> actionList = node.getActionList();
|
||||||
|
if (actionList.size() > 0) {
|
||||||
|
ArrayList<Integer> actions = new ArrayList<>();
|
||||||
|
for (AccessibilityNodeInfo.AccessibilityAction action : actionList) {
|
||||||
|
actions.add(action.getId());
|
||||||
|
}
|
||||||
|
result.put("actions", actions);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 544 B |
Binary file not shown.
After Width: | Height: | Size: 442 B |
Binary file not shown.
After Width: | Height: | Size: 721 B |
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,29 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1536M
|
6
dev/integration_tests/android_semantics_testing/android/gradle/wrapper/gradle-wrapper.properties
vendored
Executable file
6
dev/integration_tests/android_semantics_testing/android/gradle/wrapper/gradle-wrapper.properties
vendored
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#Fri Jun 23 08:50:38 CEST 2017
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
@ -0,0 +1,15 @@
|
|||||||
|
include ':app'
|
||||||
|
|
||||||
|
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||||
|
|
||||||
|
def plugins = new Properties()
|
||||||
|
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||||
|
if (pluginsFile.exists()) {
|
||||||
|
pluginsFile.withInputStream { stream -> plugins.load(stream) }
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.each { name, path ->
|
||||||
|
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||||
|
include ":$name"
|
||||||
|
project(":$name").projectDir = pluginDirectory
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2018 The Chromium 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:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
|
|
||||||
|
import 'src/tests/controls_page.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
enableFlutterDriverExtension(handler: dataHandler);
|
||||||
|
runApp(const TestApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
const MethodChannel kSemanticsChannel = MethodChannel('semantics');
|
||||||
|
|
||||||
|
Future<String> dataHandler(String message) async {
|
||||||
|
if (message.contains('getSemanticsNode')) {
|
||||||
|
final int id = int.tryParse(message.split('#')[1]) ?? 0;
|
||||||
|
final dynamic result = await kSemanticsChannel.invokeMethod('getSemanticsNode', <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
});
|
||||||
|
return json.encode(result);
|
||||||
|
}
|
||||||
|
throw new UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<String> routes = <String>[
|
||||||
|
selectionControlsRoute,
|
||||||
|
];
|
||||||
|
|
||||||
|
class TestApp extends StatelessWidget {
|
||||||
|
const TestApp();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new MaterialApp(
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
selectionControlsRoute: (BuildContext context) => new SelectionControlsPage(),
|
||||||
|
},
|
||||||
|
home: new Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
body: new Column(
|
||||||
|
children: routes.map((String value) {
|
||||||
|
return new MaterialButton(
|
||||||
|
child: new Text(value),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ class AndroidSemanticsNode {
|
|||||||
/// "isLongClickable": bool,
|
/// "isLongClickable": bool,
|
||||||
/// },
|
/// },
|
||||||
/// "text": String,
|
/// "text": String,
|
||||||
|
/// "contentDescription": String,
|
||||||
/// "className": String,
|
/// "className": String,
|
||||||
/// "id": int,
|
/// "id": int,
|
||||||
/// "rect": {
|
/// "rect": {
|
||||||
@ -64,6 +65,16 @@ class AndroidSemanticsNode {
|
|||||||
/// the Flutter [SemanticsNode].
|
/// the Flutter [SemanticsNode].
|
||||||
String get text => _values['text'];
|
String get text => _values['text'];
|
||||||
|
|
||||||
|
/// The contentDescription of the semantics node.
|
||||||
|
///
|
||||||
|
/// This field is used for the Switch, Radio, and Checkbox widgets
|
||||||
|
/// instead of [text]. If the text property is used for these, TalkBack
|
||||||
|
/// will not read out the "checked" or "not checked" label by default.
|
||||||
|
///
|
||||||
|
/// This is produced by combining the value, label, and hint fields from
|
||||||
|
/// the Flutter [SemanticsNode].
|
||||||
|
String get contentDescription => _values['contentDescription'];
|
||||||
|
|
||||||
/// The className of the semantics node.
|
/// The className of the semantics node.
|
||||||
///
|
///
|
||||||
/// Certain kinds of Flutter semantics are mapped to Android classes to
|
/// Certain kinds of Flutter semantics are mapped to Android classes to
|
||||||
|
@ -21,6 +21,9 @@ class AndroidClassName {
|
|||||||
|
|
||||||
/// The class name used for read only text fields.
|
/// The class name used for read only text fields.
|
||||||
static const String textView = 'android.widget.TextView';
|
static const String textView = 'android.widget.TextView';
|
||||||
|
|
||||||
|
/// The class name used for toggle switches.
|
||||||
|
static const String toggleSwitch = 'android.widget.Switch';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action constants which correspond to `AccessibilityAction` in Android.
|
/// Action constants which correspond to `AccessibilityAction` in Android.
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// Defines a 'package:test' shim.
|
||||||
|
// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed
|
||||||
|
|
||||||
|
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
import 'package:test/test.dart' as test_package show TypeMatcher;
|
||||||
|
|
||||||
|
export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
|
||||||
|
/// A matcher that compares the type of the actual value to the type argument T.
|
||||||
|
Matcher isInstanceOf<T>() => new test_package.TypeMatcher<T>(); // ignore: prefer_const_constructors, https://github.com/dart-lang/sdk/issues/32544
|
@ -2,10 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'common.dart';
|
import 'common.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
|
import 'flutter_test_alternative.dart';
|
||||||
|
|
||||||
/// Matches an [AndroidSemanticsNode].
|
/// Matches an [AndroidSemanticsNode].
|
||||||
///
|
///
|
||||||
@ -16,6 +15,7 @@ import 'constants.dart';
|
|||||||
/// the Flutter framework.
|
/// the Flutter framework.
|
||||||
Matcher hasAndroidSemantics({
|
Matcher hasAndroidSemantics({
|
||||||
String text,
|
String text,
|
||||||
|
String contentDescription,
|
||||||
String className,
|
String className,
|
||||||
int id,
|
int id,
|
||||||
Rect rect,
|
Rect rect,
|
||||||
@ -33,6 +33,7 @@ Matcher hasAndroidSemantics({
|
|||||||
}) {
|
}) {
|
||||||
return new _AndroidSemanticsMatcher(
|
return new _AndroidSemanticsMatcher(
|
||||||
text: text,
|
text: text,
|
||||||
|
contentDescription: contentDescription,
|
||||||
className: className,
|
className: className,
|
||||||
rect: rect,
|
rect: rect,
|
||||||
size: size,
|
size: size,
|
||||||
@ -52,6 +53,7 @@ Matcher hasAndroidSemantics({
|
|||||||
class _AndroidSemanticsMatcher extends Matcher {
|
class _AndroidSemanticsMatcher extends Matcher {
|
||||||
_AndroidSemanticsMatcher({
|
_AndroidSemanticsMatcher({
|
||||||
this.text,
|
this.text,
|
||||||
|
this.contentDescription,
|
||||||
this.className,
|
this.className,
|
||||||
this.id,
|
this.id,
|
||||||
this.actions,
|
this.actions,
|
||||||
@ -69,6 +71,7 @@ class _AndroidSemanticsMatcher extends Matcher {
|
|||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
final String className;
|
final String className;
|
||||||
|
final String contentDescription;
|
||||||
final int id;
|
final int id;
|
||||||
final List<AndroidSemanticsAction> actions;
|
final List<AndroidSemanticsAction> actions;
|
||||||
final Rect rect;
|
final Rect rect;
|
||||||
@ -87,6 +90,8 @@ class _AndroidSemanticsMatcher extends Matcher {
|
|||||||
description.add('AndroidSemanticsNode');
|
description.add('AndroidSemanticsNode');
|
||||||
if (text != null)
|
if (text != null)
|
||||||
description.add(' with text: $text');
|
description.add(' with text: $text');
|
||||||
|
if (contentDescription != null)
|
||||||
|
description.add( 'with contentDescription $contentDescription');
|
||||||
if (className != null)
|
if (className != null)
|
||||||
description.add(' with className: $className');
|
description.add(' with className: $className');
|
||||||
if (id != null)
|
if (id != null)
|
||||||
@ -118,6 +123,8 @@ class _AndroidSemanticsMatcher extends Matcher {
|
|||||||
bool matches(covariant AndroidSemanticsNode item, Map<Object, Object> matchState) {
|
bool matches(covariant AndroidSemanticsNode item, Map<Object, Object> matchState) {
|
||||||
if (text != null && text != item.text)
|
if (text != null && text != item.text)
|
||||||
return _failWithMessage('Expected text: $text', matchState);
|
return _failWithMessage('Expected text: $text', matchState);
|
||||||
|
if (contentDescription != null && contentDescription != item.contentDescription)
|
||||||
|
return _failWithMessage('Expected contentDescription: $contentDescription', matchState);
|
||||||
if (className != null && className != item.className)
|
if (className != null && className != item.className)
|
||||||
return _failWithMessage('Expected className: $className', matchState);
|
return _failWithMessage('Expected className: $className', matchState);
|
||||||
if (id != null && id != item.id)
|
if (id != null && id != item.id)
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
/// The name of the route containing the test suite.
|
||||||
|
const String selectionControlsRoute = 'controls';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the enabled checkbox.
|
||||||
|
const String checkboxKeyValue = 'SelectionControls#Checkbox1';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the disabled checkbox.
|
||||||
|
const String disabledCheckboxKeyValue = 'SelectionControls#Checkbox2';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the radio button with value 1.
|
||||||
|
const String radio1KeyValue = 'SelectionControls#Radio1';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the radio button with value 2.
|
||||||
|
const String radio2KeyValue = 'SelectionControls#Radio2';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the radio button with value 3.
|
||||||
|
const String radio3KeyValue = 'SelectionControls#Radio3';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the switch.
|
||||||
|
const String switchKeyValue = 'SelectionControls#Switch1';
|
||||||
|
|
||||||
|
/// The string supplied to the [ValueKey] for the labeled switch.
|
||||||
|
const String labeledSwitchKeyValue = 'SelectionControls#Switch2';
|
||||||
|
|
||||||
|
/// The label of the labeled switch.
|
||||||
|
const String switchLabel = 'Label';
|
@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2018 The Chromium 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 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'controls_constants.dart';
|
||||||
|
export 'controls_constants.dart';
|
||||||
|
|
||||||
|
/// A test page with a checkbox, three radio buttons, and a switch.
|
||||||
|
class SelectionControlsPage extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => new _SelectionControlsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectionControlsPageState extends State<SelectionControlsPage> {
|
||||||
|
static const ValueKey<String> checkbox1Key = ValueKey<String>(checkboxKeyValue);
|
||||||
|
static const ValueKey<String> checkbox2Key = ValueKey<String>(disabledCheckboxKeyValue);
|
||||||
|
static const ValueKey<String> radio1Key = ValueKey<String>(radio1KeyValue);
|
||||||
|
static const ValueKey<String> radio2Key = ValueKey<String>(radio2KeyValue);
|
||||||
|
static const ValueKey<String> radio3Key = ValueKey<String>(radio3KeyValue);
|
||||||
|
static const ValueKey<String> switchKey = ValueKey<String>(switchKeyValue);
|
||||||
|
static const ValueKey<String> labeledSwitchKey = ValueKey<String>(labeledSwitchKeyValue);
|
||||||
|
bool _isChecked = false;
|
||||||
|
bool _isOn = false;
|
||||||
|
bool _isLabeledOn = false;
|
||||||
|
int _radio = 0;
|
||||||
|
|
||||||
|
void _updateCheckbox(bool newValue) {
|
||||||
|
setState(() {
|
||||||
|
_isChecked = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateRadio(int newValue) {
|
||||||
|
setState(() {
|
||||||
|
_radio = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateSwitch(bool newValue) {
|
||||||
|
setState(() {
|
||||||
|
_isOn = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateLabeledSwitch(bool newValue) {
|
||||||
|
setState(() {
|
||||||
|
_isLabeledOn = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new Scaffold(
|
||||||
|
appBar: new AppBar(leading: const BackButton(key: ValueKey<String>('back'))),
|
||||||
|
body: new Material(
|
||||||
|
child: new Column(children: <Widget>[
|
||||||
|
new Row(
|
||||||
|
children: <Widget>[
|
||||||
|
new Checkbox(
|
||||||
|
key: checkbox1Key,
|
||||||
|
value: _isChecked,
|
||||||
|
onChanged: _updateCheckbox,
|
||||||
|
),
|
||||||
|
const Checkbox(
|
||||||
|
key: checkbox2Key,
|
||||||
|
value: false,
|
||||||
|
onChanged: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
new Row(children: <Widget>[
|
||||||
|
new Radio<int>(key: radio1Key, value: 0, groupValue: _radio, onChanged: _updateRadio),
|
||||||
|
new Radio<int>(key: radio2Key, value: 1, groupValue: _radio, onChanged: _updateRadio),
|
||||||
|
new Radio<int>(key: radio3Key, value: 2, groupValue: _radio, onChanged: _updateRadio),
|
||||||
|
]),
|
||||||
|
const Spacer(),
|
||||||
|
new Switch(
|
||||||
|
key: switchKey,
|
||||||
|
value: _isOn,
|
||||||
|
onChanged: _updateSwitch,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
new MergeSemantics(
|
||||||
|
child: new Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(switchLabel),
|
||||||
|
new Switch(
|
||||||
|
key: labeledSwitchKey,
|
||||||
|
value: _isLabeledOn,
|
||||||
|
onChanged: _updateLabeledSwitch,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
export 'src/tests/controls_constants.dart';
|
@ -26,4 +26,7 @@ dependencies:
|
|||||||
vm_service_client: 0.2.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
vm_service_client: 0.2.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
web_socket_channel: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
web_socket_channel: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
# PUBSPEC CHECKSUM: 2086
|
# PUBSPEC CHECKSUM: 2086
|
||||||
|
@ -19,6 +19,7 @@ const String source = r'''
|
|||||||
"isLongClickable": false
|
"isLongClickable": false
|
||||||
},
|
},
|
||||||
"text": "hello",
|
"text": "hello",
|
||||||
|
"contentDescription": "other hello",
|
||||||
"className": "android.view.View",
|
"className": "android.view.View",
|
||||||
"rect": {
|
"rect": {
|
||||||
"left": 0,
|
"left": 0,
|
||||||
@ -43,6 +44,7 @@ void main() {
|
|||||||
expect(node.isPassword, false);
|
expect(node.isPassword, false);
|
||||||
expect(node.isLongClickable, false);
|
expect(node.isLongClickable, false);
|
||||||
expect(node.text, 'hello');
|
expect(node.text, 'hello');
|
||||||
|
expect(node.contentDescription, 'other hello');
|
||||||
expect(node.id, 23);
|
expect(node.id, 23);
|
||||||
expect(node.getRect(), const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0));
|
expect(node.getRect(), const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0));
|
||||||
expect(node.getActions(), <AndroidSemanticsAction>[
|
expect(node.getActions(), <AndroidSemanticsAction>[
|
||||||
@ -78,6 +80,7 @@ void main() {
|
|||||||
isPassword: false,
|
isPassword: false,
|
||||||
isLongClickable: false,
|
isLongClickable: false,
|
||||||
text: 'hello',
|
text: 'hello',
|
||||||
|
contentDescription: 'other hello',
|
||||||
className: 'android.view.View',
|
className: 'android.view.View',
|
||||||
id: 23,
|
id: 23,
|
||||||
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
|
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
// Copyright 2018 The Chromium 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:io' as io;
|
||||||
|
|
||||||
|
import 'package:android_semantics_testing/test_constants.dart';
|
||||||
|
import 'package:android_semantics_testing/android_semantics_testing.dart';
|
||||||
|
|
||||||
|
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AccessibilityBridge', () {
|
||||||
|
FlutterDriver driver;
|
||||||
|
Future<AndroidSemanticsNode> getSemantics(SerializableFinder finder) async {
|
||||||
|
final int id = await driver.getSemanticsId(finder);
|
||||||
|
final String data = await driver.requestData('getSemanticsNode#$id');
|
||||||
|
return new AndroidSemanticsNode.deserialize(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
driver = await FlutterDriver.connect();
|
||||||
|
// Say the magic words..
|
||||||
|
final io.Process run = await io.Process.start('adb', const <String>[
|
||||||
|
'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('adb', const <String>[
|
||||||
|
'shell',
|
||||||
|
'settings',
|
||||||
|
'put',
|
||||||
|
'secure',
|
||||||
|
'enabled_accessibility_services',
|
||||||
|
'null',
|
||||||
|
]);
|
||||||
|
await run.exitCode;
|
||||||
|
driver?.close();
|
||||||
|
});
|
||||||
|
group('SelectionControls', () {
|
||||||
|
setUpAll(() async {
|
||||||
|
await driver.tap(find.text(selectionControlsRoute));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Checkbox has correct Android semantics', () async {
|
||||||
|
expect(await getSemantics(find.byValueKey(checkboxKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.checkBox,
|
||||||
|
isChecked: false,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
await driver.tap(find.byValueKey(checkboxKeyValue));
|
||||||
|
|
||||||
|
expect(await getSemantics(find.byValueKey(checkboxKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.checkBox,
|
||||||
|
isChecked: true,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
expect(await getSemantics(find.byValueKey(disabledCheckboxKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.checkBox,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: false,
|
||||||
|
actions: const <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
test('Radio has correct Android semantics', () async {
|
||||||
|
expect(await getSemantics(find.byValueKey(radio2KeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.radio,
|
||||||
|
isChecked: false,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
await driver.tap(find.byValueKey(radio2KeyValue));
|
||||||
|
|
||||||
|
expect(await getSemantics(find.byValueKey(radio2KeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.radio,
|
||||||
|
isChecked: true,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
test('Switch has correct Android semantics', () async {
|
||||||
|
expect(await getSemantics(find.byValueKey(switchKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.toggleSwitch,
|
||||||
|
isChecked: false,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
await driver.tap(find.byValueKey(switchKeyValue));
|
||||||
|
|
||||||
|
expect(await getSemantics(find.byValueKey(switchKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.toggleSwitch,
|
||||||
|
isChecked: true,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/20820.
|
||||||
|
test('Switch can be labeled', () async {
|
||||||
|
expect(await getSemantics(find.byValueKey(labeledSwitchKeyValue)), hasAndroidSemantics(
|
||||||
|
className: AndroidClassName.toggleSwitch,
|
||||||
|
isChecked: false,
|
||||||
|
isCheckable: true,
|
||||||
|
isEnabled: true,
|
||||||
|
isFocusable: true,
|
||||||
|
contentDescription: switchLabel,
|
||||||
|
actions: <AndroidSemanticsAction>[
|
||||||
|
AndroidSemanticsAction.click,
|
||||||
|
AndroidSemanticsAction.accessibilityFocus,
|
||||||
|
],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await driver.tap(find.byValueKey('back'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user