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,
|
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
|
/// Parts of the system that can generate pointer events that reach the test
|
||||||
/// binding.
|
/// 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.
|
/// Base class for bindings used by widgets library tests.
|
||||||
///
|
///
|
||||||
/// The [ensureInitialized] method creates (if necessary) and returns an
|
/// The [ensureInitialized] method creates (if necessary) and returns an
|
||||||
@ -611,6 +642,24 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
late StackTraceDemangler _oldStackTraceDemangler;
|
late StackTraceDemangler _oldStackTraceDemangler;
|
||||||
FlutterErrorDetails? _pendingExceptionDetails;
|
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(
|
static const TextStyle _messageStyle = TextStyle(
|
||||||
color: Color(0xFF917FFF),
|
color: Color(0xFF917FFF),
|
||||||
fontSize: 40.0,
|
fontSize: 40.0,
|
||||||
@ -700,6 +749,24 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
// The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console.
|
// 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> _runTest(
|
||||||
Future<void> Function() testBody,
|
Future<void> Function() testBody,
|
||||||
VoidCallback invariantTester,
|
VoidCallback invariantTester,
|
||||||
@ -707,6 +774,16 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
) {
|
) {
|
||||||
assert(description != null);
|
assert(description != null);
|
||||||
assert(inTest);
|
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;
|
_oldExceptionHandler = FlutterError.onError;
|
||||||
_oldStackTraceDemangler = FlutterError.demangleStackTrace;
|
_oldStackTraceDemangler = FlutterError.demangleStackTrace;
|
||||||
int exceptionCount = 0; // number of un-taken exceptions
|
int exceptionCount = 0; // number of un-taken exceptions
|
||||||
@ -988,6 +1065,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
_parentZone = null;
|
_parentZone = null;
|
||||||
buildOwner!.focusManager.dispose();
|
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;
|
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||||
buildOwner!.focusManager = FocusManager()..registerGlobalHandlers();
|
buildOwner!.focusManager = FocusManager()..registerGlobalHandlers();
|
||||||
|
|
||||||
|
@ -946,6 +946,13 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
return binding.takeException();
|
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.
|
/// Acts as if the application went idle.
|
||||||
///
|
///
|
||||||
/// Runs all remaining microtasks, including those scheduled as a result of
|
/// 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/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:test_api/src/expect/async_matcher.dart'; // ignore: implementation_imports
|
import 'package:test_api/src/expect/async_matcher.dart'; // ignore: implementation_imports
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
@ -821,6 +822,78 @@ void main() {
|
|||||||
binding.postTest();
|
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 {
|
class FakeMatcher extends AsyncMatcher {
|
||||||
|
Loading…
Reference in New Issue
Block a user