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),
+      ));
+    });
+  });
+}