matcher.dart 7.08 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'common.dart';
import 'constants.dart';
7
import 'flutter_test_alternative.dart';
8 9 10

/// Matches an [AndroidSemanticsNode].
///
11 12 13 14
/// Any properties which aren't supplied are ignored during the comparison,
/// with the exception of `isHeading`. The heading property is not available
/// on all versions of Android. If it is not available on the tested version,
/// it will be match whatever it is compared against.
15 16 17 18 19 20
///
/// 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,
21
  String contentDescription,
22 23 24 25 26 27 28 29 30 31 32 33
  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,
34
  bool isHeading,
35 36 37
  bool isPassword,
  bool isLongClickable,
}) {
38
  return _AndroidSemanticsMatcher(
39
    text: text,
40
    contentDescription: contentDescription,
41 42 43 44 45 46 47 48 49 50 51
    className: className,
    rect: rect,
    size: size,
    id: id,
    actions: actions,
    isChecked: isChecked,
    isCheckable: isCheckable,
    isEditable: isEditable,
    isEnabled: isEnabled,
    isFocusable: isFocusable,
    isFocused: isFocused,
52
    isHeading: isHeading,
53 54 55 56 57 58 59 60
    isPassword: isPassword,
    isLongClickable: isLongClickable,
  );
}

class _AndroidSemanticsMatcher extends Matcher {
  _AndroidSemanticsMatcher({
    this.text,
61
    this.contentDescription,
62 63 64 65 66 67 68 69 70 71 72
    this.className,
    this.id,
    this.actions,
    this.rect,
    this.size,
    this.isChecked,
    this.isCheckable,
    this.isEnabled,
    this.isEditable,
    this.isFocusable,
    this.isFocused,
73
    this.isHeading,
74 75 76 77 78 79
    this.isPassword,
    this.isLongClickable,
  });

  final String text;
  final String className;
80
  final String contentDescription;
81 82 83 84 85 86 87 88 89 90
  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;
91
  final bool isHeading;
92 93 94 95 96 97 98 99
  final bool isPassword;
  final bool isLongClickable;

  @override
  Description describe(Description description) {
    description.add('AndroidSemanticsNode');
    if (text != null)
      description.add(' with text: $text');
100 101
    if (contentDescription != null)
      description.add( 'with contentDescription $contentDescription');
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
    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');
122 123
    if (isHeading != null)
      description.add(' with flag isHeading: $isHeading');
124 125 126 127 128 129 130 131 132 133 134
    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);
135 136
    if (contentDescription != null && contentDescription != item.contentDescription)
      return _failWithMessage('Expected contentDescription: $contentDescription', matchState);
137 138 139 140 141 142 143 144 145 146
    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();
147 148 149 150 151 152 153
      if (!unorderedEquals(actions).matches(itemActions, matchState)) {
        final List<String> actionsString = actions.map<String>((AndroidSemanticsAction action) => action.toString()).toList()..sort();
        final List<String> itemActionsString = itemActions.map<String>((AndroidSemanticsAction action) => action.toString()).toList()..sort();
        final Set<String> unexpected = itemActionsString.toSet().difference(actionsString.toSet());
        final Set<String> missing = actionsString.toSet().difference(itemActionsString.toSet());
        return _failWithMessage('Expected actions: $actionsString\nActual actions: $itemActionsString\nUnexpected: $unexpected\nMissing: $missing', matchState);
      }
154 155 156 157 158 159 160 161 162 163 164 165 166
    }
    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);
167 168 169
    // Heading is not available in all Android versions, so match anything if it is not set by the platform
    if (isHeading != null && isHeading != item.isHeading && item.isHeading != null)
      return _failWithMessage('Expected isHeading: $isHeading', matchState);
170 171 172 173 174 175 176 177 178 179
    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) {
180
    return mismatchDescription.add(matchState['failure'] as String);
181 182 183 184 185 186 187
  }

  bool _failWithMessage(String value, Map<dynamic, dynamic> matchState) {
    matchState['failure'] = value;
    return false;
  }
}