Unverified Commit 235a3252 authored by nbayati's avatar nbayati Committed by GitHub

Provide test API for accessibility announcements (#109661)

parent 609b8f32
......@@ -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.
///
......@@ -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
......@@ -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,
......@@ -700,6 +749,24 @@ 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,
......@@ -707,6 +774,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
) {
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
......@@ -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();
......
......@@ -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
......
......@@ -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
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment