Skip to content

Commit

Permalink
Provide test API for accessibility announcements (#109661)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbayati committed Oct 26, 2022
1 parent 609b8f3 commit 235a325
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 0 deletions.
86 changes: 86 additions & 0 deletions packages/flutter_test/lib/src/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ enum EnginePhase {
sendSemanticsUpdate,
}

/// Signature of callbacks used to intercept messages on a given channel.
///
/// See [TestDefaultBinaryMessenger.setMockDecodedMessageHandler] for more details.
typedef _MockMessageHandler = Future<void> Function(Object?);

/// Parts of the system that can generate pointer events that reach the test
/// binding.
///
Expand Down Expand Up @@ -106,6 +111,32 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding {
}
}

/// Accessibility announcement data passed to [SemanticsService.announce] captured in a test.
///
/// This class is intended to be used by the testing API to store the announcements
/// in a structured form so that tests can verify announcement details. The fields
/// of this class correspond to parameters of the [SemanticsService.announce] method.
///
/// See also:
///
/// * [WidgetTester.takeAnnouncements], which is the test API that uses this class.
class CapturedAccessibilityAnnouncement {
const CapturedAccessibilityAnnouncement._(
this.message,
this.textDirection,
this.assertiveness,
);

/// The accessibility message announced by the framework.
final String message;

/// The direction in which the text of the [message] flows.
final TextDirection textDirection;

/// Determines the assertiveness level of the accessibility announcement.
final Assertiveness assertiveness;
}

/// Base class for bindings used by widgets library tests.
///
/// The [ensureInitialized] method creates (if necessary) and returns an
Expand Down Expand Up @@ -611,6 +642,24 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
late StackTraceDemangler _oldStackTraceDemangler;
FlutterErrorDetails? _pendingExceptionDetails;

_MockMessageHandler? _announcementHandler;
List<CapturedAccessibilityAnnouncement> _announcements =
<CapturedAccessibilityAnnouncement>[];

/// {@template flutter.flutter_test.TakeAccessibilityAnnouncements}
/// Returns a list of all the accessibility announcements made by the Flutter
/// framework since the last time this function was called.
///
/// It's safe to call this when there hasn't been any announcements; it will return
/// an empty list in that case.
/// {@endtemplate}
List<CapturedAccessibilityAnnouncement> takeAnnouncements() {
assert(inTest);
final List<CapturedAccessibilityAnnouncement> announcements = _announcements;
_announcements = <CapturedAccessibilityAnnouncement>[];
return announcements;
}

static const TextStyle _messageStyle = TextStyle(
color: Color(0xFF917FFF),
fontSize: 40.0,
Expand Down Expand Up @@ -700,13 +749,41 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
// The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console.
}

Future<void> _handleAnnouncementMessage(Object? mockMessage) async {
final Map<Object?, Object?> message = mockMessage! as Map<Object?, Object?>;
if (message['type'] == 'announce') {
final Map<Object?, Object?> data =
message['data']! as Map<Object?, Object?>;
final String dataMessage = data['message'].toString();
final TextDirection textDirection =
TextDirection.values[data['textDirection']! as int];
final int assertivenessLevel = (data['assertiveness'] as int?) ?? 0;
final Assertiveness assertiveness =
Assertiveness.values[assertivenessLevel];
final CapturedAccessibilityAnnouncement announcement =
CapturedAccessibilityAnnouncement._(
dataMessage, textDirection, assertiveness);
_announcements.add(announcement);
}
}

Future<void> _runTest(
Future<void> Function() testBody,
VoidCallback invariantTester,
String description,
) {
assert(description != null);
assert(inTest);

// Set the handler only if there is currently none.
if (TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.checkMockMessageHandler(SystemChannels.accessibility.name, null)) {
_announcementHandler = _handleAnnouncementMessage;
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(
SystemChannels.accessibility, _announcementHandler);
}

_oldExceptionHandler = FlutterError.onError;
_oldStackTraceDemangler = FlutterError.demangleStackTrace;
int exceptionCount = 0; // number of un-taken exceptions
Expand Down Expand Up @@ -988,6 +1065,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
_parentZone = null;
buildOwner!.focusManager.dispose();

if (TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.checkMockMessageHandler(
SystemChannels.accessibility.name, _announcementHandler)) {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockDecodedMessageHandler(SystemChannels.accessibility, null);
_announcementHandler = null;
}
_announcements = <CapturedAccessibilityAnnouncement>[];

ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
buildOwner!.focusManager = FocusManager()..registerGlobalHandlers();

Expand Down
7 changes: 7 additions & 0 deletions packages/flutter_test/lib/src/widget_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,13 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
return binding.takeException();
}

/// {@macro flutter.flutter_test.TakeAccessibilityAnnouncements}
///
/// See [TestWidgetsFlutterBinding.takeAnnouncements] for details.
List<CapturedAccessibilityAnnouncement> takeAnnouncements() {
return binding.takeAnnouncements();
}

/// Acts as if the application went idle.
///
/// Runs all remaining microtasks, including those scheduled as a result of
Expand Down
73 changes: 73 additions & 0 deletions packages/flutter_test/test/widget_tester_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test_api/src/expect/async_matcher.dart'; // ignore: implementation_imports
// ignore: deprecated_member_use
Expand Down Expand Up @@ -821,6 +822,78 @@ void main() {
binding.postTest();
});
});

group('Accessibility announcements testing API', () {
testWidgets('Returns the list of announcements', (WidgetTester tester) async {

// Make sure the handler is properly set
expect(TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.checkMockMessageHandler(SystemChannels.accessibility.name, null), isFalse);

await SemanticsService.announce('announcement 1', TextDirection.ltr);
await SemanticsService.announce('announcement 2', TextDirection.rtl,
assertiveness: Assertiveness.assertive);
await SemanticsService.announce('announcement 3', TextDirection.rtl);

final List<CapturedAccessibilityAnnouncement> list = tester.takeAnnouncements();
expect(list, hasLength(3));
final CapturedAccessibilityAnnouncement first = list[0];
expect(first.message, 'announcement 1');
expect(first.textDirection, TextDirection.ltr);

final CapturedAccessibilityAnnouncement second = list[1];
expect(second.message, 'announcement 2');
expect(second.textDirection, TextDirection.rtl);
expect(second.assertiveness, Assertiveness.assertive);

final CapturedAccessibilityAnnouncement third = list[2];
expect(third.message, 'announcement 3');
expect(third.textDirection, TextDirection.rtl);
expect(third.assertiveness, Assertiveness.polite);

final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements();
expect(emptyList, <CapturedAccessibilityAnnouncement>[]);
});

test('New test API is not breaking existing tests', () async {
final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[];

Future<dynamic> handleMessage(dynamic mockMessage) async {
final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>;
log.add(message);
}

TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(
SystemChannels.accessibility, handleMessage);

await SemanticsService.announce('announcement 1', TextDirection.rtl,
assertiveness: Assertiveness.assertive);
expect(
log,
equals(<Map<String, dynamic>>[
<String, dynamic>{
'type': 'announce',
'data': <String, dynamic>{
'message': 'announcement 1',
'textDirection': 0,
'assertiveness': 1
}
},
]));

// Remove the handler
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.setMockDecodedMessageHandler<dynamic>(
SystemChannels.accessibility, null);
});

tearDown(() {
// Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest]
expect(TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
.checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue);
});
});
}

class FakeMatcher extends AsyncMatcher {
Expand Down

0 comments on commit 235a325

Please sign in to comment.