| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:test/test.dart' hide isInstanceOf; |
| |
| import 'common.dart'; |
| import 'constants.dart'; |
| |
| /// Matches an [AndroidSemanticsNode]. |
| /// |
| /// 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. |
| /// |
| /// 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? contentDescription, |
| String? className, |
| int? id, |
| Rect? rect, |
| Size? size, |
| List<AndroidSemanticsAction>? actions, |
| List<AndroidSemanticsAction>? ignoredActions, |
| List<AndroidSemanticsNode>? children, |
| bool? isChecked, |
| bool? isCheckable, |
| bool? isEditable, |
| bool? isEnabled, |
| bool? isFocusable, |
| bool? isFocused, |
| bool? isHeading, |
| bool? isPassword, |
| bool? isLongClickable, |
| }) { |
| return _AndroidSemanticsMatcher( |
| text: text, |
| contentDescription: contentDescription, |
| className: className, |
| rect: rect, |
| size: size, |
| id: id, |
| actions: actions, |
| ignoredActions: ignoredActions, |
| isChecked: isChecked, |
| isCheckable: isCheckable, |
| isEditable: isEditable, |
| isEnabled: isEnabled, |
| isFocusable: isFocusable, |
| isFocused: isFocused, |
| isHeading: isHeading, |
| isPassword: isPassword, |
| isLongClickable: isLongClickable, |
| ); |
| } |
| |
| class _AndroidSemanticsMatcher extends Matcher { |
| _AndroidSemanticsMatcher({ |
| this.text, |
| this.contentDescription, |
| this.className, |
| this.id, |
| this.actions, |
| this.ignoredActions, |
| this.rect, |
| this.size, |
| this.isChecked, |
| this.isCheckable, |
| this.isEnabled, |
| this.isEditable, |
| this.isFocusable, |
| this.isFocused, |
| this.isHeading, |
| this.isPassword, |
| this.isLongClickable, |
| }); |
| |
| final String? text; |
| final String? className; |
| final String? contentDescription; |
| final int? id; |
| final List<AndroidSemanticsAction>? actions; |
| final List<AndroidSemanticsAction>? ignoredActions; |
| 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? isHeading; |
| final bool? isPassword; |
| final bool? isLongClickable; |
| |
| @override |
| Description describe(Description description) { |
| description.add('AndroidSemanticsNode'); |
| if (text != null) { |
| description.add(' with text: $text'); |
| } |
| if (contentDescription != null) { |
| description.add( 'with contentDescription $contentDescription'); |
| } |
| 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 (isHeading != null) { |
| description.add(' with flag isHeading: $isHeading'); |
| } |
| 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<dynamic, dynamic> matchState) { |
| if (text != null && text != item.text) { |
| return _failWithMessage('Expected text: $text', matchState); |
| } |
| if (contentDescription != null && contentDescription != item.contentDescription) { |
| return _failWithMessage('Expected contentDescription: $contentDescription', 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)) { |
| 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<AndroidSemanticsAction> unexpected = itemActions.toSet().difference(actions!.toSet()); |
| final Set<String> unexpectedInString = itemActionsString.toSet().difference(actionsString.toSet()); |
| final Set<String> missingInString = actionsString.toSet().difference(itemActionsString.toSet()); |
| if (missingInString.isEmpty && ignoredActions != null && unexpected.every(ignoredActions!.contains)) { |
| return true; |
| } |
| return _failWithMessage('Expected actions: $actionsString\nActual actions: $itemActionsString\nUnexpected: $unexpectedInString\nMissing: $missingInString', 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); |
| } |
| // 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); |
| } |
| 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(dynamic item, Description mismatchDescription, |
| Map<dynamic, dynamic> matchState, bool verbose) { |
| final String? failure = matchState['failure'] as String?; |
| if (failure == null) { |
| return mismatchDescription.add('hasAndroidSemantics matcher does not complete successfully'); |
| } |
| return mismatchDescription.add(failure); |
| } |
| |
| bool _failWithMessage(String value, Map<dynamic, dynamic> matchState) { |
| matchState['failure'] = value; |
| return false; |
| } |
| } |