New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide test API for accessibility announcements #109661
Conversation
| @@ -106,6 +111,14 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding { | |||
| } | |||
| } | |||
|
|
|||
| /// An accessibility announcement. | |||
| class Announcement { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename this to CapturedAccessibilityAnnouncement. Otherwise it sounds too much like a production class name. Also, "announcement" does not by itself imply accessibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only potential issue would be lines getting longer than 80 character. I could always break lines similar to what dart formatter would do though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| /// Returns a list announcements made by the Flutter framework. | ||
| List<Announcement>? takeAnnouncements() { | ||
| assert(inTest); | ||
| return _announcements; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the semantics of "take" means "remove the returned value from the captured list".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah you/re right. I've changed the code to clear out the list afterwards. I don't think we'd need a peak/get function for list. What do you think?
| } | ||
|
|
||
| /// Returns the most recent announcement made by the Flutter framework. | ||
| Announcement? getLastAnnouncement(){ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is meant to return the last announcement without removing it, I'd call it peekLastAnnouncement rather than get. But I think perhaps we want take here as well, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So take would remove the announcement from the list? In that case if the user calls takeLastAnnouncement twice, the second time what they'll get is not actually the last announcement. I'm not sure if this can cause confusion or if it's a common practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point. In that case maybe it should be called just takeAnnouncement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remember correctly, we decided that we don't need both takeAnnouncement and takeAnnouncements. So I just changed the name of the function to peekLastAnnouncement without changing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the other comments, I'm removing this function all together because it's easy to access the last announcement through takeAnnouncements() and we don't really need a separate function.
| @@ -700,13 +727,40 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| // The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console. | |||
| } | |||
|
|
|||
| Future<dynamic> _handleMessage(dynamic mockMessage) async { | |||
| final Map<dynamic, dynamic> data = | |||
| (mockMessage as Map<dynamic, dynamic>)['data'] as Map<dynamic, dynamic>; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember, are the keys in the map really dynamic? I recall them all being Strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried both
final Map<String, dynamic> data = (mockMessage as Map<dynamic, dynamic>)['data'] as Map<String, dynamic>;
and
final Map<String, dynamic> data = (mockMessage as Map<String, dynamic>)['data'] as Map<String, dynamic>;
In both cases I get a casting error:
the following _CastError was thrown running a test: type '_InternalLinkedHashMap<Object?, Object?>' is not a subtype of type 'Map<String, dynamic>' in type cast
Is there a better way to cast the mockMessage and get the values inside data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use .cast<K, V> to cast to a certain type https://api.flutter.dev/flutter/dart-core/Map/cast.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, interesting. In that case it's fine to keep it dynamic. @chunhtai's suggestion also works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both mockMessage and mockMessage['data'] are dynamic. It seems like cast<K,V>() is defined for Map and there isn't a cast function defined on dynamic type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nbayati you can call any method on dynamic. So you can do mockMessage.cast<String, dynamic>() and it should work as long as mockMessage actually has that method.
| @@ -700,13 +727,40 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| // The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console. | |||
| } | |||
|
|
|||
| Future<dynamic> _handleMessage(dynamic mockMessage) async { | |||
| final Map<dynamic, dynamic> data = | |||
| (mockMessage as Map<dynamic, dynamic>)['data'] as Map<dynamic, dynamic>; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use .cast<K, V> to cast to a certain type https://api.flutter.dev/flutter/dart-core/Map/cast.html
| expect(TextDirection.rtl, equals(third.textDirection)); | ||
| expect(Assertiveness.polite, equals(third.assertiveness)); | ||
|
|
||
| final List<CapturedAccessibilityAnnouncement>? emptyList = tester.takeAnnouncements(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call to check that the list was emptied out!
| expect(emptyList, isNull); | ||
| }); | ||
|
|
||
| test('New test API is not breaking existing tests', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do plan to break existing tests in the future by always specifying assertiveness in the protocol, right? So I'm not sure if this test provides real protection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial thought was to make sure that the current way of testing still works in general. Meaning that if someone wants to setup their own message handler, they're still able to do so. In another word, the new API doesn't overwrite what the user has specified as the message handler.
You had a good point about the future change though. So I've removed the announcements that didn't have assertiveness specified, since having only one would still accomplish what I had in mind.
Let me know if you still think we don't need this test.
|
(triage): @nbayati do you still have plans to follow up on the feedback given above? |
Yes. I have been focusing on another high priority PR, but this is still on my list and I should be able to get back to it pretty soon. |
| @@ -62,6 +62,11 @@ enum EnginePhase { | |||
| sendSemanticsUpdate, | |||
| } | |||
|
|
|||
| /// Signature of callbacks to intercept messages on a given channel. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "...callbacks used to intercept..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| @@ -62,6 +62,11 @@ enum EnginePhase { | |||
| sendSemanticsUpdate, | |||
| } | |||
|
|
|||
| /// Signature of callbacks to intercept messages on a given channel. | |||
| /// | |||
| /// see [TestDefaultBinaryMessenger.setMockDecodedMessageHandler] for more details. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/see/See/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| @@ -106,6 +111,32 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding { | |||
| } | |||
| } | |||
|
|
|||
| /// An accessibility announcement made by the Flutter framework. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this class does not represent the announcement itself, but the data captured in the test. I'd describe it as "Accessibility announcement data passed to [SemanticsService.announce] captured in a test."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion. I wasn't sure how to word this one.
| @@ -106,6 +111,32 @@ mixin TestDefaultBinaryMessengerBinding on BindingBase, ServicesBinding { | |||
| } | |||
| } | |||
|
|
|||
| /// An accessibility announcement made by the Flutter framework. | |||
| /// | |||
| /// This class is mostly used by the testing api to store the announcements | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/mostly/intended to be/
nit: s/api/API/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| /// An accessibility announcement made by the Flutter framework. | ||
| /// | ||
| /// This class is mostly used by the testing api to store the announcements | ||
| /// in a user-friendly structure so that the code consumers can check the details |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: hopefully all our API is user-friendly
nit: "code consumer" sounds confusing. Maybe say "so that tests can verify announcement details"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestions, Done!
| return announcements; | ||
| } | ||
|
|
||
| /// Returns the most recent announcement made by the Flutter framework. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs should also explain what it means when this method returns null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the function to return an empty list in that case. Do you still have concerns with the documentation?
| Future<void> _runTest( | ||
| Future<void> Function() testBody, | ||
| VoidCallback invariantTester, | ||
| String description, | ||
| ) { | ||
| assert(description != null); | ||
| assert(inTest); | ||
|
|
||
| if (TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider leaving a comment explaining what this does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| @@ -821,6 +822,78 @@ void main() { | |||
| binding.postTest(); | |||
| }); | |||
| }); | |||
|
|
|||
| group('Accessibility announcements testing API', () { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also test that clean-up logic from postTest works. Since the clean-up takes place as part of testWidgets after the test code is done, it can be done in tearDown, for example:
tearDown(() {
expect(checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue);
});There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| @@ -821,6 +822,78 @@ void main() { | |||
| binding.postTest(); | |||
| }); | |||
| }); | |||
|
|
|||
| group('Accessibility announcements testing API', () { | |||
| testWidgets('Returns last announcement', (WidgetTester tester) async { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, as a sanity check, we could verify that the mock handler is installed, for example:
expect(checkMockMessageHandler(SystemChannels.accessibility.name, null), isFalse);There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! Added the sanity check to the test.
| }, | ||
| ])); | ||
|
|
||
| //remove the handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: // Remove...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| /// Signature of callbacks to intercept messages on a given channel. | ||
| /// | ||
| /// see [TestDefaultBinaryMessenger.setMockDecodedMessageHandler] for more details. | ||
| typedef _MockMessageHandler = Future<dynamic> Function(dynamic); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please avoid dynamic. It turns off dart's type system. If you don't have a more specific type, Object? should work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, done!
| /// | ||
| /// See [TestWidgetsFlutterBinding.takeAnnouncements]. | ||
| class CapturedAccessibilityAnnouncement { | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove blank line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| @@ -700,13 +751,43 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| // The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console. | |||
| } | |||
|
|
|||
| Future<dynamic> _handleMessage(dynamic mockMessage) async { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future<void> as return type and Object? mockMessage as argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! Done!
| @@ -700,13 +751,43 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| // The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console. | |||
| } | |||
|
|
|||
| Future<dynamic> _handleMessage(dynamic mockMessage) async { | |||
| final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and elsewhere: Object? instead of dynamic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| final CapturedAccessibilityAnnouncement announcement; | ||
|
|
||
| announcement = CapturedAccessibilityAnnouncement( | ||
| dataMessage, textDirection, | ||
| assertiveness: assertiveness); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
combine this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
|
|
||
| /// Returns the most recent announcement made by the Flutter framework. | ||
| CapturedAccessibilityAnnouncement? peekLastAnnouncement() { | ||
| return _announcements?.last; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this have the inTest assert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this function based on the other comments.
| /// Returns the most recent announcement made by the Flutter framework. | ||
| /// | ||
| /// See [TestWidgetsFlutterBinding.peekLastAnnouncement] for details. | ||
| CapturedAccessibilityAnnouncement? peekLastAnnouncement() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need both of these APIs? Wouldn't it be enough to just have the takeAnnouncements one? If we go with both, the API docs should link to the other one in a "see also" section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Removed peekLastAnnouncement() since having the takeAnnouncements() seems enough.
LGTM but since this is a framework-side change that affects more than just the web, I think we should also look for an approval from either @chunhtai or @goderbauer.
| /// | ||
| /// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/the/The/
Separate paragraphs by one empty line. Or if this was meant to be a continuation of the previous paragraph, there's still space on the previous line to begin this sentence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| /// The accessibility message announced by the framework. | ||
| final String message; | ||
|
|
||
| /// The direction in which text flows. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd add "the text of the [message] flows"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| @@ -700,13 +747,41 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| // The LiveTestWidgetsFlutterBinding overrides this to report the exception to the console. | |||
| } | |||
|
|
|||
| Future<void> _handleMessage(Object? mockMessage) async { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _handleMessage sounds too generic. This is handling announcement messages specifically. Maybe call it _handleAnnouncementMessage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
| /// Returns a list of all the accessibility announcements made by the Flutter | ||
| /// framework since the last time this function was called. | ||
| /// | ||
| /// See [TestWidgetsFlutterBinding.takeAnnouncements] for details. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the documentation can be identical, instead of pointing to another place, you can use dartdoc templates. Here's an example:
- Create a template:
/// {@template flutter.cupertino.CupertinoNavigationBar.leading} - Copy it to another place using a macro:
/// {@macro flutter.cupertino.CupertinoNavigationBar.leading}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool feature! Thanks for pointing it out.
| @@ -611,6 +642,24 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
| late StackTraceDemangler _oldStackTraceDemangler; | |||
| FlutterErrorDetails? _pendingExceptionDetails; | |||
|
|
|||
| _MockMessageHandler? _announcementHandler; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this variable? Could you just use _handleAnnouncementMessage directly wherever it is used?
| expect(third.assertiveness, Assertiveness.polite); | ||
|
|
||
| final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements(); | ||
| expect(emptyList, <CapturedAccessibilityAnnouncement>[]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: There is the isEmpty matcher, which I think would make this slightly more readable:
| expect(emptyList, <CapturedAccessibilityAnnouncement>[]); | |
| expect(emptyList, isEmpty); |
Provide testing API for the accessibility announcements made by the Flutter framework. Currently the consumers of the code have to create a mock message handler and match the exact
Stringvalue of theannouncementwhich is aMap. So adding any new features to the announcements (like: #107568) would break the existing tests. This PR addresses the issue by providing a testing API which takes care of setting up a mock handler and returns an object for easy comparison.Fixes #108495
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.