From 1a0594fd3d51d145a413ea8a12829e9c0c37090b Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Thu, 1 Dec 2022 14:44:03 -0800 Subject: [PATCH 1/4] [web] use a permanent live region for a11y announcements --- lib/web_ui/lib/src/engine/initialization.dart | 2 + .../src/engine/semantics/accessibility.dart | 113 ++++++++++-------- .../engine/semantics/accessibility_test.dart | 95 +++++++-------- 3 files changed, 110 insertions(+), 100 deletions(-) diff --git a/lib/web_ui/lib/src/engine/initialization.dart b/lib/web_ui/lib/src/engine/initialization.dart index d332523896cb9..ef0ad54ab1a1b 100644 --- a/lib/web_ui/lib/src/engine/initialization.dart +++ b/lib/web_ui/lib/src/engine/initialization.dart @@ -17,6 +17,7 @@ import 'package:ui/src/engine/profiler.dart'; import 'package:ui/src/engine/raw_keyboard.dart'; import 'package:ui/src/engine/renderer.dart'; import 'package:ui/src/engine/safe_browser_api.dart'; +import 'package:ui/src/engine/semantics/accessibility.dart'; import 'package:ui/src/engine/window.dart'; import 'package:ui/ui.dart' as ui; @@ -240,6 +241,7 @@ Future initializeEngineUi() async { } _initializationState = DebugEngineInitializationState.initializingUi; + initializeAccessibilityAnnouncements(); RawKeyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs); MouseCursor.initialize(); ensureFlutterViewEmbedderInitialized(); diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index 22d93833b8ec8..e399dac3d990f 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:typed_data'; import '../../engine.dart' show registerHotRestartListener; @@ -21,84 +20,100 @@ enum Assertiveness { } /// Singleton for accessing accessibility announcements from the platform. -final AccessibilityAnnouncements accessibilityAnnouncements = - AccessibilityAnnouncements.instance; +AccessibilityAnnouncements get accessibilityAnnouncements { + assert( + _accessibilityAnnouncements != null, + 'AccessibilityAnnouncements not initialized. Call initializeAccessibilityAnnouncements() to innitialize it.', + ); + return _accessibilityAnnouncements!; +} +AccessibilityAnnouncements? _accessibilityAnnouncements; + +void initializeAccessibilityAnnouncements() { + assert( + _accessibilityAnnouncements == null, + 'AccessibilityAnnouncements is already initialized. This is likely a bug in ' + 'Flutter Web engine initialization. Please file an issue at ' + 'https://github.com/flutter/flutter/issues/new/choose', + ); + _accessibilityAnnouncements = AccessibilityAnnouncements(); + registerHotRestartListener(() { + accessibilityAnnouncements.dispose(); + }); +} /// Attaches accessibility announcements coming from the 'flutter/accessibility' /// channel as temporary elements to the DOM. class AccessibilityAnnouncements { - AccessibilityAnnouncements._() { - registerHotRestartListener(() { - _removeElementTimer?.cancel(); - }); + factory AccessibilityAnnouncements() { + final DomHTMLElement politeElement = _createElement(Assertiveness.polite); + final DomHTMLElement assertiveElement = _createElement(Assertiveness.assertive); + domDocument.body!.append(politeElement); + domDocument.body!.append(assertiveElement); + return AccessibilityAnnouncements._(politeElement, assertiveElement); } - /// Initializes the [AccessibilityAnnouncements] singleton if it is not - /// already initialized. - static AccessibilityAnnouncements get instance { - return _instance ??= AccessibilityAnnouncements._(); - } + AccessibilityAnnouncements._(this._politeElement, this._assertiveElement); - static AccessibilityAnnouncements? _instance; + /// A live region element with `aria-live` set to "polite", used to announce + /// accouncements politely. + final DomHTMLElement _politeElement; - /// Timer that times when the accessibility element should be removed from the - /// DOM. - /// - /// The element is added to the DOM temporarily for announcing the - /// message to the assistive technology. - Timer? _removeElementTimer; + /// A live region element with `aria-live` set to "assertive", used to announce + /// accouncements assertively. + final DomHTMLElement _assertiveElement; - /// The duration the accessibility announcements stay on the DOM. - /// - /// It is removed after this time expired. - Duration durationA11yMessageIsOnDom = const Duration(seconds: 5); + DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) { + assert(!_isDisposed); + switch (assertiveness) { + case Assertiveness.polite: return _politeElement; + case Assertiveness.assertive: return _assertiveElement; + } + } - /// Element which is used to communicate the message from the - /// 'flutter/accessibility' to the assistive technologies. - /// - /// This element gets attached to the DOM temporarily. It gets removed - /// after a duration. See [durationA11yMessageIsOnDom]. - /// - /// This element has aria-live attribute. - /// - /// It also has id 'accessibility-element' for testing purposes. - DomHTMLElement? _element; + bool _isDisposed = false; - DomHTMLElement get _domElement => _element ??= _createElement(); + void dispose() { + assert(!_isDisposed); + _isDisposed = true; + _politeElement.remove(); + _assertiveElement.remove(); + _accessibilityAnnouncements = null; + } /// Decodes the message coming from the 'flutter/accessibility' channel. void handleMessage(StandardMessageCodec codec, ByteData? data) { + assert(!_isDisposed); final Map inputMap = codec.decodeMessage(data) as Map; final Map dataMap = inputMap.readDynamicJson('data'); final String? message = dataMap.tryString('message'); if (message != null && message.isNotEmpty) { /// The default value for politeness is `polite`. - final int ariaLivePolitenessIndex = dataMap.tryInt('assertiveness') ?? 0; - final Assertiveness ariaLivePoliteness = Assertiveness.values[ariaLivePolitenessIndex]; - _initLiveRegion(message, ariaLivePoliteness); - _removeElementTimer = Timer(durationA11yMessageIsOnDom, () { - _element!.remove(); - }); - } - } + final int assertivenessIndex = dataMap.tryInt('assertiveness') ?? 0; + final Assertiveness assertiveness = Assertiveness.values[assertivenessIndex]; + final DomHTMLElement liveRegion = ariaLiveElementFor(assertiveness); - void _initLiveRegion(String message, Assertiveness ariaLivePoliteness) { - final String assertiveLevel = (ariaLivePoliteness == Assertiveness.assertive) ? 'assertive' : 'polite'; - _domElement.setAttribute('aria-live', assertiveLevel); - _domElement.text = message; - domDocument.body!.append(_domElement); + // Create an additional child so that if the text of the announcement is + // the same as before, it is announced again. + final DomHTMLDivElement textContainer = createDomHTMLDivElement(); + textContainer.text = message; + liveRegion.clearChildren(); + liveRegion.appendChild(textContainer); + } } - DomHTMLLabelElement _createElement() { + static DomHTMLLabelElement _createElement(Assertiveness assertiveness) { + final String ariaLiveValue = (assertiveness == Assertiveness.assertive) ? 'assertive' : 'polite'; final DomHTMLLabelElement liveRegion = createDomHTMLLabelElement(); - liveRegion.setAttribute('id', 'accessibility-element'); + liveRegion.setAttribute('id', 'ftl-announcement-$ariaLiveValue'); liveRegion.style ..position = 'fixed' ..overflow = 'hidden' ..transform = 'translate(-99999px, -99999px)' ..width = '1px' ..height = '1px'; + liveRegion.setAttribute('aria-live', ariaLiveValue); return liveRegion; } } diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index c8b2059f202ea..bca07ffa5bdc8 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -2,90 +2,83 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async' show Future; - import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine/dom.dart'; +import 'package:ui/src/engine/initialization.dart'; import 'package:ui/src/engine/semantics.dart'; import 'package:ui/src/engine/services.dart'; const StandardMessageCodec codec = StandardMessageCodec(); -const String testMessage = 'This is an tooltip.'; -const Map testInput = { - 'data': {'message': testMessage} -}; void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() { - late AccessibilityAnnouncements accessibilityAnnouncements; + setUpAll(() async { + await initializeEngine(); + }); group('$AccessibilityAnnouncements', () { - setUp(() { - accessibilityAnnouncements = AccessibilityAnnouncements.instance; - }); - - test( - 'Creates element when handling a message and removes ' - 'is after a delay', () { - // Set the a11y announcement's duration on DOM to half seconds. - accessibilityAnnouncements.durationA11yMessageIsOnDom = - const Duration(milliseconds: 500); - - // Initially there is no accessibility-element - expect(domDocument.getElementById('accessibility-element'), isNull); - - accessibilityAnnouncements.handleMessage(codec, - codec.encodeMessage(testInput)); + void expectAnnouncementElements({required bool present}) { + expect( + domDocument.getElementById('ftl-announcement-polite'), + present ? isNotNull : isNull, + ); expect( - domDocument.getElementById('accessibility-element'), - isNotNull, + domDocument.getElementById('ftl-announcement-assertive'), + present ? isNotNull : isNull, ); - final DomHTMLLabelElement input = - domDocument.getElementById('accessibility-element')! as DomHTMLLabelElement; - expect(input.getAttribute('aria-live'), equals('polite')); - expect(input.text, testMessage); + } - // The element should have been removed after the duration. - Future.delayed( - accessibilityAnnouncements.durationA11yMessageIsOnDom, - () => - expect(domDocument.getElementById('accessibility-element'), isNull)); + test('Initialization and disposal', () { + // Elements should be there right after engine initialization. + expectAnnouncementElements(present: true); + + accessibilityAnnouncements.dispose(); + expectAnnouncementElements(present: false); + + initializeAccessibilityAnnouncements(); + expectAnnouncementElements(present: true); }); + void resetAccessibilityAnnouncements() { + accessibilityAnnouncements.dispose(); + initializeAccessibilityAnnouncements(); + expectAnnouncementElements(present: true); + } + test('Default value of aria-live is polite when assertiveness is not specified', () { - const Map testInput = {'data': {'message': 'message'}}; + resetAccessibilityAnnouncements(); + const Map testInput = {'data': {'message': 'polite message'}}; accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput)); - final DomHTMLLabelElement input = domDocument.getElementById('accessibility-element')! as DomHTMLLabelElement; - - expect(input.getAttribute('aria-live'), equals('polite')); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); - test('aria-live is assertive when assertiveness is set to 1', () { - const Map testInput = {'data': {'message': 'message', 'assertiveness': 1}}; + test('aria-live is assertive when assertiveness is set to 1', () { + resetAccessibilityAnnouncements(); + const Map testInput = {'data': {'message': 'assertive message', 'assertiveness': 1}}; accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput)); - final DomHTMLLabelElement input = domDocument.getElementById('accessibility-element')! as DomHTMLLabelElement; - - expect(input.getAttribute('aria-live'), equals('assertive')); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, ''); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, 'assertive message'); }); test('aria-live is polite when assertiveness is null', () { - const Map testInput = {'data': {'message': 'message', 'assertiveness': null}}; + resetAccessibilityAnnouncements(); + const Map testInput = {'data': {'message': 'polite message', 'assertiveness': null}}; accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput)); - final DomHTMLLabelElement input = domDocument.getElementById('accessibility-element')! as DomHTMLLabelElement; - - expect(input.getAttribute('aria-live'), equals('polite')); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); test('aria-live is polite when assertiveness is set to 0', () { - const Map testInput = {'data': {'message': 'message', 'assertiveness': 0}}; + resetAccessibilityAnnouncements(); + const Map testInput = {'data': {'message': 'polite message', 'assertiveness': 0}}; accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput)); - final DomHTMLLabelElement input = domDocument.getElementById('accessibility-element')! as DomHTMLLabelElement; - - expect(input.getAttribute('aria-live'), equals('polite')); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); }); } From 2c8e007f82d418a55a191c1b3b1a5a117edc21bc Mon Sep 17 00:00:00 2001 From: Yegor Date: Thu, 1 Dec 2022 20:27:21 -0800 Subject: [PATCH 2/4] alter same message to force screen reader to read it again --- .../lib/src/engine/semantics/accessibility.dart | 12 ++++++------ .../test/engine/semantics/accessibility_test.dart | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index e399dac3d990f..6fc3205219c83 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -94,12 +94,12 @@ class AccessibilityAnnouncements { final Assertiveness assertiveness = Assertiveness.values[assertivenessIndex]; final DomHTMLElement liveRegion = ariaLiveElementFor(assertiveness); - // Create an additional child so that if the text of the announcement is - // the same as before, it is announced again. - final DomHTMLDivElement textContainer = createDomHTMLDivElement(); - textContainer.text = message; - liveRegion.clearChildren(); - liveRegion.appendChild(textContainer); + // If the last announced message is the same as the new message, some + // screen readers, such as Narrator, will not read the same message + // again. In this case, add an artifical "!" at the end of the message + // string to force the text of the message to look different. + final String suffix = liveRegion.innerText == message ? '!' : ''; + liveRegion.text = '$message$suffix'; } } diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index bca07ffa5bdc8..e45a4bbbdd470 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -80,5 +80,18 @@ void testMain() { expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message'); expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); + + test('The same message announced twice is altered to convince the screen reader to read it again.', () { + resetAccessibilityAnnouncements(); + const Map testInput = {'data': {'message': 'Hello'}}; + accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput)); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); + + const Map testInput2 = {'data': {'message': 'Hello'}}; + accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput2)); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello!'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); + }); }); } From 859b9563f33ad2927f18c68d4601e23327fd2047 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Fri, 2 Dec 2022 14:57:09 -0800 Subject: [PATCH 3/4] use period instead of exclamation mark --- lib/web_ui/lib/src/engine/semantics/accessibility.dart | 4 ++-- lib/web_ui/test/engine/semantics/accessibility_test.dart | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index 6fc3205219c83..9236a9c701f21 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -96,9 +96,9 @@ class AccessibilityAnnouncements { // If the last announced message is the same as the new message, some // screen readers, such as Narrator, will not read the same message - // again. In this case, add an artifical "!" at the end of the message + // again. In this case, add an artifical "." at the end of the message // string to force the text of the message to look different. - final String suffix = liveRegion.innerText == message ? '!' : ''; + final String suffix = liveRegion.innerText == message ? '.' : ''; liveRegion.text = '$message$suffix'; } } diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index e45a4bbbdd470..312d28e8f3e5f 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -88,9 +88,16 @@ void testMain() { expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello'); expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); + // The DOM value gains a "." to make the message look updated. const Map testInput2 = {'data': {'message': 'Hello'}}; accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput2)); - expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello!'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello.'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); + + // Now the "." is removed because the message without it will also look updated. + const Map testInput3 = {'data': {'message': 'Hello'}}; + accessibilityAnnouncements.handleMessage(codec, codec.encodeMessage(testInput3)); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello'); expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); }); From 11dbd7bc7a48e462b63c65d170ced335dbf73d9c Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Fri, 2 Dec 2022 19:51:18 -0800 Subject: [PATCH 4/4] docs --- .../src/engine/semantics/accessibility.dart | 48 +++++++++++++------ .../engine/semantics/accessibility_test.dart | 14 ++++++ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/accessibility.dart b/lib/web_ui/lib/src/engine/semantics/accessibility.dart index 9236a9c701f21..9f95393eb2397 100644 --- a/lib/web_ui/lib/src/engine/semantics/accessibility.dart +++ b/lib/web_ui/lib/src/engine/semantics/accessibility.dart @@ -29,6 +29,10 @@ AccessibilityAnnouncements get accessibilityAnnouncements { } AccessibilityAnnouncements? _accessibilityAnnouncements; +/// Initializes the [accessibilityAnnouncements] singleton. +/// +/// It is an error to attempt to initialize the singleton more than once. Call +/// [AccessibilityAnnouncements.dispose] prior to calling this function again. void initializeAccessibilityAnnouncements() { assert( _accessibilityAnnouncements == null, @@ -42,9 +46,9 @@ void initializeAccessibilityAnnouncements() { }); } -/// Attaches accessibility announcements coming from the 'flutter/accessibility' -/// channel as temporary elements to the DOM. +/// Makes accessibility announcements using `aria-live` DOM elements. class AccessibilityAnnouncements { + /// Creates a new instance with its own DOM elements used for announcements. factory AccessibilityAnnouncements() { final DomHTMLElement politeElement = _createElement(Assertiveness.polite); final DomHTMLElement assertiveElement = _createElement(Assertiveness.assertive); @@ -63,6 +67,7 @@ class AccessibilityAnnouncements { /// accouncements assertively. final DomHTMLElement _assertiveElement; + /// Looks up the element used to announce messages of the given [assertiveness]. DomHTMLElement ariaLiveElementFor(Assertiveness assertiveness) { assert(!_isDisposed); switch (assertiveness) { @@ -73,6 +78,9 @@ class AccessibilityAnnouncements { bool _isDisposed = false; + /// Disposes of the resources used by this object. + /// + /// This object's methods must not be called after calling this method. void dispose() { assert(!_isDisposed); _isDisposed = true; @@ -81,28 +89,40 @@ class AccessibilityAnnouncements { _accessibilityAnnouncements = null; } - /// Decodes the message coming from the 'flutter/accessibility' channel. + /// Makes an accessibity announcement from a message sent by the framework + /// over the 'flutter/accessibility' channel. + /// + /// The encoded message is passed as [data], and will be decoded using [codec]. void handleMessage(StandardMessageCodec codec, ByteData? data) { assert(!_isDisposed); - final Map inputMap = - codec.decodeMessage(data) as Map; + final Map inputMap = codec.decodeMessage(data) as Map; final Map dataMap = inputMap.readDynamicJson('data'); final String? message = dataMap.tryString('message'); if (message != null && message.isNotEmpty) { - /// The default value for politeness is `polite`. + /// The default value for assertiveness is `polite`. final int assertivenessIndex = dataMap.tryInt('assertiveness') ?? 0; final Assertiveness assertiveness = Assertiveness.values[assertivenessIndex]; - final DomHTMLElement liveRegion = ariaLiveElementFor(assertiveness); - - // If the last announced message is the same as the new message, some - // screen readers, such as Narrator, will not read the same message - // again. In this case, add an artifical "." at the end of the message - // string to force the text of the message to look different. - final String suffix = liveRegion.innerText == message ? '.' : ''; - liveRegion.text = '$message$suffix'; + announce(message, assertiveness); } } + /// Makes an accessibility announcement using an `aria-live` element. + /// + /// [message] is the text of the announcement. + /// + /// [assertiveness] controls how interruptive the announcement is. + void announce(String message, Assertiveness assertiveness) { + assert(!_isDisposed); + final DomHTMLElement ariaLiveElement = ariaLiveElementFor(assertiveness); + + // If the last announced message is the same as the new message, some + // screen readers, such as Narrator, will not read the same message + // again. In this case, add an artifical "." at the end of the message + // string to force the text of the message to look different. + final String suffix = ariaLiveElement.innerText == message ? '.' : ''; + ariaLiveElement.text = '$message$suffix'; + } + static DomHTMLLabelElement _createElement(Assertiveness assertiveness) { final String ariaLiveValue = (assertiveness == Assertiveness.assertive) ? 'assertive' : 'polite'; final DomHTMLLabelElement liveRegion = createDomHTMLLabelElement(); diff --git a/lib/web_ui/test/engine/semantics/accessibility_test.dart b/lib/web_ui/test/engine/semantics/accessibility_test.dart index 312d28e8f3e5f..99176120a5b37 100644 --- a/lib/web_ui/test/engine/semantics/accessibility_test.dart +++ b/lib/web_ui/test/engine/semantics/accessibility_test.dart @@ -100,5 +100,19 @@ void testMain() { expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'Hello'); expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); }); + + test('announce() polite', () { + resetAccessibilityAnnouncements(); + accessibilityAnnouncements.announce('polite message', Assertiveness.polite); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, 'polite message'); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, ''); + }); + + test('announce() assertive', () { + resetAccessibilityAnnouncements(); + accessibilityAnnouncements.announce('assertive message', Assertiveness.assertive); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.polite).text, ''); + expect(accessibilityAnnouncements.ariaLiveElementFor(Assertiveness.assertive).text, 'assertive message'); + }); }); }