Create flutter_driver infra for testing the Android AccessibilityNodeInfo generated by Flutter (#19700)
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 5492210..c4cb09a 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -309,6 +309,7 @@
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'));
+ await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world'));
diff --git a/dev/integration_tests/android_semantics_testing/lib/android_semantics_testing.dart b/dev/integration_tests/android_semantics_testing/lib/android_semantics_testing.dart
new file mode 100644
index 0000000..226cc5c
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/android_semantics_testing.dart
@@ -0,0 +1,11 @@
+// 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.
+
+/// This library provides constants and matchers for testing the Android
+/// accessibility implementation in a flutter driver environment.
+library android_semantics_testing;
+
+export 'src/common.dart';
+export 'src/constants.dart';
+export 'src/matcher.dart';
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/common.dart b/dev/integration_tests/android_semantics_testing/lib/src/common.dart
new file mode 100644
index 0000000..a088cda
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/src/common.dart
@@ -0,0 +1,219 @@
+// 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:convert';
+
+import 'package:meta/meta.dart';
+import 'constants.dart';
+
+/// A semantics node created from Android accessibility information.
+///
+/// This object represents Android accessibility information derived from an
+/// [AccessibilityNodeInfo](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo)
+/// object. The purpose is to verify in integration
+/// tests that our semantics framework produces the correct accessibility info
+/// on Android.
+///
+/// See also:
+///
+/// * [AccessibilityNodeInfo](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo)
+class AndroidSemanticsNode {
+ AndroidSemanticsNode._(this._values);
+
+ /// Deserializes a new [AndroidSemanticsNode] from a json map.
+ ///
+ /// The structure of the JSON:
+ ///
+ /// {
+ /// "flags": {
+ /// "isChecked": bool,
+ /// "isCheckable": bool,
+ /// "isEditable": bool,
+ /// "isEnabled": bool,
+ /// "isFocusable": bool,
+ /// "isFocused": bool,
+ /// "isPassword": bool,
+ /// "isLongClickable": bool,
+ /// },
+ /// "text": String,
+ /// "className": String,
+ /// "id": int,
+ /// "rect": {
+ /// left: int,
+ /// top: int,
+ /// right: int,
+ /// bottom: int,
+ /// },
+ /// actions: [
+ /// int,
+ /// ]
+ /// }
+ factory AndroidSemanticsNode.deserialize(String value) {
+ return new AndroidSemanticsNode._(json.decode(value));
+ }
+
+ final Map<String, Object> _values;
+ final List<AndroidSemanticsNode> _children = <AndroidSemanticsNode>[];
+
+ Map<String, Object> get _flags => _values['flags'];
+
+ /// The text value of the semantics node.
+ ///
+ /// This is produced by combining the value, label, and hint fields from
+ /// the Flutter [SemanticsNode].
+ String get text => _values['text'];
+
+ /// The className of the semantics node.
+ ///
+ /// Certain kinds of Flutter semantics are mapped to Android classes to
+ /// use their default semantic behavior, such as checkboxes and images.
+ ///
+ /// If a more specific value isn't provided, it defaults to
+ /// "android.view.View".
+ String get className => _values['className'];
+
+ /// The identifier for this semantics node.
+ int get id => _values['id'];
+
+ /// The children of this semantics node.
+ List<AndroidSemanticsNode> get children => _children;
+
+ /// Whether the node is currently in a checked state.
+ ///
+ /// Equivalent to [SemanticsFlag.isChecked].
+ bool get isChecked => _flags['isChecked'];
+
+ /// Whether the node can be in a checked state.
+ ///
+ /// Equivalent to [SemanticsFlag.hasCheckedState]
+ bool get isCheckable => _flags['isCheckable'];
+
+ /// Whether the node is editable.
+ ///
+ /// This is usually only applied to text fields, which map
+ /// to "android.widget.EditText".
+ bool get isEditable => _flags['isEditable'];
+
+ /// Whether the node is enabled.
+ bool get isEnabled => _flags['isEnabled'];
+
+ /// Whether the node is focusable.
+ bool get isFocusable => _flags['isFocusable'];
+
+ /// Whether the node is focused.
+ bool get isFocused => _flags['isFocused'];
+
+ /// Whether the node represents a password field.
+ ///
+ /// Equivalent to [SemanticsFlag.isObscured].
+ bool get isPassword => _flags['isPassword'];
+
+ /// Whether the node is long clickable.
+ ///
+ /// Equivalent to having [SemanticsAction.longPress].
+ bool get isLongClickable => _flags['isLongClickable'];
+
+ /// Gets a [Rect] which defines the position and size of the semantics node.
+ Rect getRect() {
+ final Map<String, Object> rawRect = _values['rect'];
+ final Map<String, int> rect = rawRect.cast<String, int>();
+ return new Rect.fromLTRB(
+ rect['left'].toDouble(),
+ rect['top'].toDouble(),
+ rect['right'].toDouble(),
+ rect['bottom'].toDouble(),
+ );
+ }
+
+ /// Gets a [Size] which defines the size of the semantics node.
+ Size getSize() {
+ final Rect rect = getRect();
+ return new Size(rect.bottom - rect.top, rect.right - rect.left);
+ }
+
+ /// Gets a list of [AndroidSemanticsActions] which are defined for the node.
+ List<AndroidSemanticsAction> getActions() {
+ final List<AndroidSemanticsAction> result = <AndroidSemanticsAction>[];
+ for (int id in _values['actions']) {
+ result.add(AndroidSemanticsAction.deserialize(id));
+ }
+ return result;
+ }
+
+ @override
+ String toString() {
+ return _values.toString();
+ }
+}
+
+
+/// A Dart VM implementation of a rectangle.
+///
+/// Created to mirror the implementation of [ui.Rect].
+@immutable
+class Rect {
+ /// Creates a new rectangle.
+ ///
+ /// All values are required.
+ const Rect.fromLTRB(this.left, this.top, this.right, this.bottom);
+
+ /// The top side of the rectangle.
+ final double top;
+
+ /// The left side of the rectangle.
+ final double left;
+
+ /// The right side of the rectangle.
+ final double right;
+
+ /// The bottom side of the rectangle.
+ final double bottom;
+
+ @override
+ int get hashCode =>
+ top.hashCode ^ left.hashCode ^ right.hashCode ^ bottom.hashCode;
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType)
+ return false;
+ final Rect typedOther = other;
+ return typedOther.top == top &&
+ typedOther.left == left &&
+ typedOther.right == right &&
+ typedOther.bottom == bottom;
+ }
+
+ @override
+ String toString() => 'Rect.fromLTRB($left, $top, $right, $bottom)';
+}
+
+/// A Dart VM implementation of a Size.
+///
+/// Created to mirror the implementation [ui.Size].
+@immutable
+class Size {
+ /// Creates a new [Size] object.
+ const Size(this.width, this.height);
+
+ /// The width of some object.
+ final double width;
+
+ /// The height of some object.
+ final double height;
+
+ @override
+ int get hashCode => width.hashCode ^ height.hashCode;
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType)
+ return false;
+ final Size typedOther = other;
+ return typedOther.width == width && typedOther.height == height;
+ }
+
+ @override
+ String toString() => 'Size{$width, $height}';
+}
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/constants.dart b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart
new file mode 100644
index 0000000..fb641e5
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart
@@ -0,0 +1,203 @@
+// 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:meta/meta.dart';
+
+/// Class name constants which correspond to the class names used by the
+/// Android accessibility bridge.
+class AndroidClassName {
+ /// The class name used for checkboxes.
+ static const String checkBox = 'android.widget.CheckBox';
+
+ /// The default className if none is provided by flutter.
+ static const String view = 'android.view.View';
+
+ /// The class name used for radio buttons.
+ static const String radio = 'android.widget.RadioButton';
+
+ /// The class name used for editable text fields.
+ static const String editText = 'android.widget.EditText';
+
+ /// The class name used for read only text fields.
+ static const String textView = 'android.widget.TextView';
+}
+
+/// Action constants which correspond to `AccessibilityAction` in Android.
+@immutable
+class AndroidSemanticsAction {
+ const AndroidSemanticsAction._(this.id);
+
+ /// The Android id of the action.
+ final int id;
+
+ static const int _kFocusIndex = 1 << 0;
+ static const int _kClearFocusIndex = 1 << 1;
+ static const int _kSelectIndex = 1 << 2;
+ static const int _kClearSelectionIndex = 1 << 3;
+ static const int _kClickIndex = 1 << 4;
+ static const int _kLongClickIndex = 1 << 5;
+ static const int _kAccessibilityFocusIndex = 1 << 6;
+ static const int _kClearAccessibilityFocusIndex = 1 << 7;
+ static const int _kNextAtMovementGranularityIndex = 1 << 8;
+ static const int _kPreviousAtMovementGranularityIndex = 1 << 9;
+ static const int _kNextHtmlElementIndex = 1 << 10;
+ static const int _kPreviousHtmlElementIndex = 1 << 11;
+ static const int _kScrollForwardIndex = 1 << 12;
+ static const int _kScrollBackwardIndex = 1 << 13;
+ static const int _kCutIndex = 1 << 14;
+ static const int _kCopyIndex = 1 << 15;
+ static const int _kPasteIndex = 1 << 16;
+ static const int _kSetSelectionIndex = 1 << 17;
+ static const int _kExpandIndex = 1 << 18;
+ static const int _kCollapseIndex = 1 << 19;
+
+ /// Matches `AccessibilityAction.ACTION_FOCUS`.
+ static const AndroidSemanticsAction focus = AndroidSemanticsAction._(_kFocusIndex);
+
+ /// Matches `AccessibilityAction.ACTION_CLEAR_FOCUS`.
+ static const AndroidSemanticsAction clearFocus = AndroidSemanticsAction._(_kClearFocusIndex);
+
+ /// Matches `AccessibilityAction.ACTION_SELECT`.
+ static const AndroidSemanticsAction select = AndroidSemanticsAction._(_kSelectIndex);
+
+ /// Matches `AccessibilityAction.ACTION_CLEAR_SELECTION`.
+ static const AndroidSemanticsAction clearSelection = AndroidSemanticsAction._(_kClearSelectionIndex);
+
+ /// Matches `AccessibilityAction.ACTION_CLICK`.
+ static const AndroidSemanticsAction click = AndroidSemanticsAction._(_kClickIndex);
+
+ /// Matches `AccessibilityAction.ACTION_LONG_CLICK`.
+ static const AndroidSemanticsAction longClick = AndroidSemanticsAction._(_kLongClickIndex);
+
+ /// Matches `AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS`.
+ static const AndroidSemanticsAction accessibilityFocus = AndroidSemanticsAction._(_kAccessibilityFocusIndex);
+
+ /// Matches `AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS`.
+ static const AndroidSemanticsAction clearAccessibilityFocus = AndroidSemanticsAction._(_kClearAccessibilityFocusIndex);
+
+ /// Matches `AccessibilityAction.ACTION_NEXT_AT_MOVEMENT_GRANULARITY`.
+ static const AndroidSemanticsAction nextAtMovementGranularity = AndroidSemanticsAction._(_kNextAtMovementGranularityIndex);
+
+ /// Matches `AccessibilityAction.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY`.
+ static const AndroidSemanticsAction previousAtMovementGranularity = AndroidSemanticsAction._(_kPreviousAtMovementGranularityIndex);
+
+ /// Matches `AccessibilityAction.ACTION_NEXT_HTML_ELEMENT`.
+ static const AndroidSemanticsAction nextHtmlElement = AndroidSemanticsAction._(_kNextHtmlElementIndex);
+
+ /// Matches `AccessibilityAction.ACTION_PREVIOUS_HTML_ELEMENT`.
+ static const AndroidSemanticsAction previousHtmlElement = AndroidSemanticsAction._(_kPreviousHtmlElementIndex);
+
+ /// Matches `AccessibilityAction.ACTION_SCROLL_FORWARD`.
+ static const AndroidSemanticsAction scrollForward = AndroidSemanticsAction._(_kScrollForwardIndex);
+
+ /// Matches `AccessibilityAction.ACTION_SCROLL_BACKWARD`.
+ static const AndroidSemanticsAction scrollBackward = AndroidSemanticsAction._(_kScrollBackwardIndex);
+
+ /// Matches `AccessibilityAction.ACTION_CUT`.
+ static const AndroidSemanticsAction cut = AndroidSemanticsAction._(_kCutIndex);
+
+ /// Matches `AccessibilityAction.ACTION_COPY`.
+ static const AndroidSemanticsAction copy = AndroidSemanticsAction._(_kCopyIndex);
+
+ /// Matches `AccessibilityAction.ACTION_PASTE`.
+ static const AndroidSemanticsAction paste = AndroidSemanticsAction._(_kPasteIndex);
+
+ /// Matches `AccessibilityAction.ACTION_SET_SELECTION`.
+ static const AndroidSemanticsAction setSelection = AndroidSemanticsAction._(_kSetSelectionIndex);
+
+ /// Matches `AccessibilityAction.ACTION_EXPAND`.
+ static const AndroidSemanticsAction expand = AndroidSemanticsAction._(_kExpandIndex);
+
+ /// Matches `AccessibilityAction.ACTION_COLLAPSE`.
+ static const AndroidSemanticsAction collapse = AndroidSemanticsAction._(_kCollapseIndex);
+
+ @override
+ String toString() {
+ switch (id) {
+ case _kFocusIndex:
+ return 'AndroidSemanticsAction.focus';
+ case _kClearFocusIndex:
+ return 'AndroidSemanticsAction.clearFocus';
+ case _kSelectIndex:
+ return 'AndroidSemanticsAction.select';
+ case _kClearSelectionIndex:
+ return 'AndroidSemanticsAction.clearSelection';
+ case _kClickIndex:
+ return 'AndroidSemanticsAction.click';
+ case _kLongClickIndex:
+ return 'AndroidSemanticsAction.longClick';
+ case _kAccessibilityFocusIndex:
+ return 'AndroidSemanticsAction.accessibilityFocus';
+ case _kClearAccessibilityFocusIndex:
+ return 'AndroidSemanticsAction.clearAccessibilityFocus';
+ case _kNextAtMovementGranularityIndex:
+ return 'AndroidSemanticsAction.nextAtMovementGranularity';
+ case _kPreviousAtMovementGranularityIndex:
+ return 'AndroidSemanticsAction.nextAtMovementGranularity';
+ case _kNextHtmlElementIndex:
+ return 'AndroidSemanticsAction.nextHtmlElement';
+ case _kPreviousHtmlElementIndex:
+ return 'AndroidSemanticsAction.previousHtmlElement';
+ case _kScrollForwardIndex:
+ return 'AndroidSemanticsAction.scrollForward';
+ case _kScrollBackwardIndex:
+ return 'AndroidSemanticsAction.scrollBackward';
+ case _kCutIndex:
+ return 'AndroidSemanticsAction.cut';
+ case _kCopyIndex:
+ return 'AndroidSemanticsAction.copy';
+ case _kPasteIndex:
+ return 'AndroidSemanticsAction.paste';
+ case _kSetSelectionIndex:
+ return 'AndroidSemanticsAction.setSelection';
+ case _kExpandIndex:
+ return 'AndroidSemanticsAction.expand';
+ case _kCollapseIndex:
+ return 'AndroidSemanticsAction.collapse';
+ default:
+ return null;
+ }
+ }
+
+ static const Map<int, AndroidSemanticsAction> _kactionById = <int, AndroidSemanticsAction>{
+ _kFocusIndex: focus,
+ _kClearFocusIndex: clearFocus,
+ _kSelectIndex: select,
+ _kClearSelectionIndex: clearSelection,
+ _kClickIndex: click,
+ _kLongClickIndex: longClick,
+ _kAccessibilityFocusIndex: accessibilityFocus,
+ _kClearAccessibilityFocusIndex: clearAccessibilityFocus,
+ _kNextAtMovementGranularityIndex: nextAtMovementGranularity,
+ _kPreviousAtMovementGranularityIndex: nextAtMovementGranularity,
+ _kNextHtmlElementIndex: nextHtmlElement,
+ _kPreviousHtmlElementIndex: previousHtmlElement,
+ _kScrollForwardIndex: scrollForward,
+ _kScrollBackwardIndex: scrollBackward,
+ _kCutIndex: cut,
+ _kCopyIndex: copy,
+ _kPasteIndex: paste,
+ _kSetSelectionIndex: setSelection,
+ _kExpandIndex: expand,
+ _kCollapseIndex: collapse,
+ };
+
+ @override
+ int get hashCode => id.hashCode;
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType)
+ return false;
+ final AndroidSemanticsAction typedOther = other;
+ return id == typedOther.id;
+ }
+
+ /// Creates a new [AndroidSemanticsAction] from an integer `value`.
+ ///
+ /// Returns `null` if the id is not a known Android accessibility action.
+ static AndroidSemanticsAction deserialize(int value) {
+ return _kactionById[value];
+ }
+}
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
new file mode 100644
index 0000000..896866c
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
@@ -0,0 +1,163 @@
+// 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_test/flutter_test.dart';
+
+import 'common.dart';
+import 'constants.dart';
+
+/// Matches an [AndroidSemanticsNode].
+///
+/// Any properties which aren't supplied are ignored during the comparison.
+///
+/// This matcher is intended to compare the accessibility values generated by
+/// the Android accessibility bridge, and not the semantics object created by
+/// the Flutter framework.
+Matcher hasAndroidSemantics({
+ String text,
+ String className,
+ int id,
+ Rect rect,
+ Size size,
+ List<AndroidSemanticsAction> actions,
+ List<AndroidSemanticsNode> children,
+ bool isChecked,
+ bool isCheckable,
+ bool isEditable,
+ bool isEnabled,
+ bool isFocusable,
+ bool isFocused,
+ bool isPassword,
+ bool isLongClickable,
+}) {
+ return new _AndroidSemanticsMatcher(
+ text: text,
+ className: className,
+ rect: rect,
+ size: size,
+ id: id,
+ actions: actions,
+ isChecked: isChecked,
+ isCheckable: isCheckable,
+ isEditable: isEditable,
+ isEnabled: isEnabled,
+ isFocusable: isFocusable,
+ isFocused: isFocused,
+ isPassword: isPassword,
+ isLongClickable: isLongClickable,
+ );
+}
+
+class _AndroidSemanticsMatcher extends Matcher {
+ _AndroidSemanticsMatcher({
+ this.text,
+ this.className,
+ this.id,
+ this.actions,
+ this.rect,
+ this.size,
+ this.isChecked,
+ this.isCheckable,
+ this.isEnabled,
+ this.isEditable,
+ this.isFocusable,
+ this.isFocused,
+ this.isPassword,
+ this.isLongClickable,
+ });
+
+ final String text;
+ final String className;
+ final int id;
+ final List<AndroidSemanticsAction> actions;
+ final Rect rect;
+ final Size size;
+ final bool isChecked;
+ final bool isCheckable;
+ final bool isEditable;
+ final bool isEnabled;
+ final bool isFocusable;
+ final bool isFocused;
+ final bool isPassword;
+ final bool isLongClickable;
+
+ @override
+ Description describe(Description description) {
+ description.add('AndroidSemanticsNode');
+ if (text != null)
+ description.add(' with text: $text');
+ if (className != null)
+ description.add(' with className: $className');
+ if (id != null)
+ description.add(' with id: $id');
+ if (actions != null)
+ description.add(' with actions: $actions');
+ if (rect != null)
+ description.add(' with rect: $rect');
+ if (size != null)
+ description.add(' with size: $size');
+ if (isChecked != null)
+ description.add(' with flag isChecked: $isChecked');
+ if (isEditable != null)
+ description.add(' with flag isEditable: $isEditable');
+ if (isEnabled != null)
+ description.add(' with flag isEnabled: $isEnabled');
+ if (isFocusable != null)
+ description.add(' with flag isFocusable: $isFocusable');
+ if (isFocused != null)
+ description.add(' with flag isFocused: $isFocused');
+ if (isPassword != null)
+ description.add(' with flag isPassword: $isPassword');
+ if (isLongClickable != null)
+ description.add(' with flag isLongClickable: $isLongClickable');
+ return description;
+ }
+
+ @override
+ bool matches(covariant AndroidSemanticsNode item, Map<Object, Object> matchState) {
+ if (text != null && text != item.text)
+ return _failWithMessage('Expected text: $text', matchState);
+ if (className != null && className != item.className)
+ return _failWithMessage('Expected className: $className', matchState);
+ if (id != null && id != item.id)
+ return _failWithMessage('Expected id: $id', matchState);
+ if (rect != null && rect != item.getRect())
+ return _failWithMessage('Expected rect: $rect', matchState);
+ if (size != null && size != item.getSize())
+ return _failWithMessage('Expected size: $size', matchState);
+ if (actions != null) {
+ final List<AndroidSemanticsAction> itemActions = item.getActions();
+ if (!unorderedEquals(actions).matches(itemActions, matchState))
+ return _failWithMessage('Expected actions: $actions', matchState);
+ }
+ if (isChecked != null && isChecked != item.isChecked)
+ return _failWithMessage('Expected isChecked: $isChecked', matchState);
+ if (isCheckable != null && isCheckable != item.isCheckable)
+ return _failWithMessage('Expected isCheckable: $isCheckable', matchState);
+ if (isEditable != null && isEditable != item.isEditable)
+ return _failWithMessage('Expected isEditable: $isEditable', matchState);
+ if (isEnabled != null && isEnabled != item.isEnabled)
+ return _failWithMessage('Expected isEnabled: $isEnabled', matchState);
+ if (isFocusable != null && isFocusable != item.isFocusable)
+ return _failWithMessage('Expected isFocusable: $isFocusable', matchState);
+ if (isFocused != null && isFocused != item.isFocused)
+ return _failWithMessage('Expected isFocused: $isFocused', matchState);
+ if (isPassword != null && isPassword != item.isPassword)
+ return _failWithMessage('Expected isPassword: $isPassword', matchState);
+ if (isLongClickable != null && isLongClickable != item.isLongClickable)
+ return _failWithMessage('Expected longClickable: $isLongClickable', matchState);
+ return true;
+ }
+
+ @override
+ Description describeMismatch(Object item, Description mismatchDescription,
+ Map<Object, Object> matchState, bool verbose) {
+ return mismatchDescription.add(matchState['failure']);
+ }
+
+ bool _failWithMessage(String value, Map<dynamic, dynamic> matchState) {
+ matchState['failure'] = value;
+ return false;
+ }
+}
diff --git a/dev/integration_tests/android_semantics_testing/pubspec.yaml b/dev/integration_tests/android_semantics_testing/pubspec.yaml
new file mode 100644
index 0000000..33453cd
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/pubspec.yaml
@@ -0,0 +1,29 @@
+name: android_semantics_testing
+description: Integration testing library for Android semantics
+
+dependencies:
+ flutter:
+ sdk: flutter
+ flutter_driver:
+ sdk: flutter
+
+ async: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ convert: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ file: 5.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ intl: 0.15.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ json_rpc_2: 2.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ meta: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ path: 1.6.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ source_span: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ stream_channel: 1.6.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+ vector_math: 2.0.8 # 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"
+
+# PUBSPEC CHECKSUM: d684
diff --git a/dev/integration_tests/android_semantics_testing/test/android_semantics_testing_test.dart b/dev/integration_tests/android_semantics_testing/test/android_semantics_testing_test.dart
new file mode 100644
index 0000000..90b9005
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/test/android_semantics_testing_test.dart
@@ -0,0 +1,93 @@
+// 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:android_semantics_testing/android_semantics_testing.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+// JSON matching a serialized Android AccessibilityNodeInfo.
+const String source = r'''
+{
+ "id": 23,
+ "flags": {
+ "isChecked": false,
+ "isCheckable": false,
+ "isEditable": false,
+ "isFocusable": false,
+ "isFocused": false,
+ "isPassword": false,
+ "isLongClickable": false
+ },
+ "text": "hello",
+ "className": "android.view.View",
+ "rect": {
+ "left": 0,
+ "top": 0,
+ "right": 10,
+ "bottom": 10
+ },
+ "actions": [1, 2, 4]
+}
+''';
+
+void main() {
+ group(AndroidSemanticsNode, () {
+ test('can be parsed from json data', () {
+ final AndroidSemanticsNode node = AndroidSemanticsNode.deserialize(source);
+
+ expect(node.isChecked, false);
+ expect(node.isCheckable, false);
+ expect(node.isEditable, false);
+ expect(node.isFocusable, false);
+ expect(node.isFocused, false);
+ expect(node.isPassword, false);
+ expect(node.isLongClickable, false);
+ expect(node.text, 'hello');
+ expect(node.id, 23);
+ expect(node.getRect(), const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0));
+ expect(node.getActions(), <AndroidSemanticsAction>[
+ AndroidSemanticsAction.focus,
+ AndroidSemanticsAction.clearFocus,
+ AndroidSemanticsAction.select,
+ ]);
+ expect(node.className, 'android.view.View');
+ expect(node.getSize(), const Size(10.0, 10.0));
+ });
+ });
+
+ group(AndroidSemanticsAction, () {
+ test('can be parsed from correct constant id', () {
+ expect(AndroidSemanticsAction.deserialize(0x1), AndroidSemanticsAction.focus);
+ });
+
+ test('returns null passed a bogus id', () {
+ expect(AndroidSemanticsAction.deserialize(23), isNull);
+ });
+ });
+
+ group('hasAndroidSemantics', () {
+ test('matches all android semantics properties', () {
+ final AndroidSemanticsNode node = AndroidSemanticsNode.deserialize(source);
+
+ expect(node, hasAndroidSemantics(
+ isChecked: false,
+ isCheckable: false,
+ isEditable: false,
+ isFocusable: false,
+ isFocused: false,
+ isPassword: false,
+ isLongClickable: false,
+ text: 'hello',
+ className: 'android.view.View',
+ id: 23,
+ rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
+ actions: <AndroidSemanticsAction>[
+ AndroidSemanticsAction.focus,
+ AndroidSemanticsAction.clearFocus,
+ AndroidSemanticsAction.select,
+ ],
+ size: const Size(10.0, 10.0),
+ ));
+ });
+ });
+}