mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Provide test API for accessibility announcements (#109661)
This commit is contained in:
parent
609b8f3219
commit
235a3252d2
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user