mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fixes Edge trigger route change announcement (#21975)
This commit is contained in:
parent
1233fe4902
commit
aed8e019be
@ -1356,16 +1356,29 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
|
||||
// Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
|
||||
// previously cached route id.
|
||||
|
||||
// Finds the last route that is not in the previous routes.
|
||||
SemanticsNode lastAdded = null;
|
||||
for (SemanticsNode semanticsNode : newRoutes) {
|
||||
if (!flutterNavigationStack.contains(semanticsNode.id)) {
|
||||
lastAdded = semanticsNode;
|
||||
}
|
||||
}
|
||||
|
||||
// If all the routes are in the previous route, get the last route.
|
||||
if (lastAdded == null && newRoutes.size() > 0) {
|
||||
lastAdded = newRoutes.get(newRoutes.size() - 1);
|
||||
}
|
||||
if (lastAdded != null && lastAdded.id != previousRouteId) {
|
||||
|
||||
// There are two cases if lastAdded != nil
|
||||
// 1. lastAdded is not in previous routes. In this case,
|
||||
// lastAdded.id != previousRouteId
|
||||
// 2. All new routes are in previous routes and
|
||||
// lastAdded = newRoutes.last.
|
||||
// In the first case, we need to announce new route. In the second case,
|
||||
// we need to announce if one list is shorter than the other.
|
||||
if (lastAdded != null
|
||||
&& (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
|
||||
previousRouteId = lastAdded.id;
|
||||
sendWindowChangeEvent(lastAdded);
|
||||
}
|
||||
|
||||
@ -142,6 +142,134 @@ public class AccessibilityBridgeTest {
|
||||
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itAnnouncesRouteNameWhenAddingNewRoute() {
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
node1.label = "node1";
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
|
||||
ArgumentCaptor<AccessibilityEvent> eventCaptor =
|
||||
ArgumentCaptor.forClass(AccessibilityEvent.class);
|
||||
verify(mockParent, times(2))
|
||||
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
|
||||
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
|
||||
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
List<CharSequence> sentences = event.getText();
|
||||
assertEquals(sentences.size(), 1);
|
||||
assertEquals(sentences.get(0).toString(), "node1");
|
||||
|
||||
TestSemanticsNode new_root = new TestSemanticsNode();
|
||||
new_root.id = 0;
|
||||
TestSemanticsNode new_node1 = new TestSemanticsNode();
|
||||
new_node1.id = 1;
|
||||
new_node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
new_node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
new_node1.label = "new_node1";
|
||||
new_root.children.add(new_node1);
|
||||
TestSemanticsNode new_node2 = new TestSemanticsNode();
|
||||
new_node2.id = 2;
|
||||
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
new_node2.label = "new_node2";
|
||||
new_node1.children.add(new_node2);
|
||||
testSemanticsUpdate = new_root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
|
||||
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
|
||||
verify(mockParent, times(4))
|
||||
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
|
||||
event = eventCaptor.getAllValues().get(2);
|
||||
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
sentences = event.getText();
|
||||
assertEquals(sentences.size(), 1);
|
||||
assertEquals(sentences.get(0).toString(), "new_node2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itAnnouncesRouteNameWhenRemoveARoute() {
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
node1.label = "node1";
|
||||
root.children.add(node1);
|
||||
TestSemanticsNode node2 = new TestSemanticsNode();
|
||||
node2.id = 2;
|
||||
node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
node2.label = "node2";
|
||||
node1.children.add(node2);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
|
||||
ArgumentCaptor<AccessibilityEvent> eventCaptor =
|
||||
ArgumentCaptor.forClass(AccessibilityEvent.class);
|
||||
verify(mockParent, times(2))
|
||||
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
|
||||
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
|
||||
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
List<CharSequence> sentences = event.getText();
|
||||
assertEquals(sentences.size(), 1);
|
||||
assertEquals(sentences.get(0).toString(), "node2");
|
||||
|
||||
TestSemanticsNode new_root = new TestSemanticsNode();
|
||||
new_root.id = 0;
|
||||
TestSemanticsNode new_node1 = new TestSemanticsNode();
|
||||
new_node1.id = 1;
|
||||
new_node1.label = "new_node1";
|
||||
new_root.children.add(new_node1);
|
||||
TestSemanticsNode new_node2 = new TestSemanticsNode();
|
||||
new_node2.id = 2;
|
||||
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
|
||||
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
|
||||
new_node2.label = "new_node2";
|
||||
new_node1.children.add(new_node2);
|
||||
testSemanticsUpdate = new_root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
|
||||
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
|
||||
verify(mockParent, times(4))
|
||||
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
|
||||
event = eventCaptor.getAllValues().get(2);
|
||||
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||
sentences = event.getText();
|
||||
assertEquals(sentences.size(), 1);
|
||||
assertEquals(sentences.get(0).toString(), "new_node2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itAnnouncesWhiteSpaceWhenNoNamesRoute() {
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
|
||||
@ -167,17 +167,27 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
|
||||
}
|
||||
NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
|
||||
[root collectRoutes:newRoutes];
|
||||
// Finds the last route that is not in the previous routes.
|
||||
for (SemanticsObject* route in newRoutes) {
|
||||
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) !=
|
||||
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
|
||||
previous_routes_.end()) {
|
||||
lastAdded = route;
|
||||
}
|
||||
}
|
||||
// If all the routes are in the previous route, get the last route.
|
||||
if (lastAdded == nil && [newRoutes count] > 0) {
|
||||
int index = [newRoutes count] - 1;
|
||||
lastAdded = [newRoutes objectAtIndex:index];
|
||||
}
|
||||
if (lastAdded != nil && [lastAdded uid] != previous_route_id_) {
|
||||
// There are two cases if lastAdded != nil
|
||||
// 1. lastAdded is not in previous routes. In this case,
|
||||
// [lastAdded uid] != previous_route_id_
|
||||
// 2. All new routes are in previous routes and
|
||||
// lastAdded = newRoutes.last.
|
||||
// In the first case, we need to announce new route. In the second case,
|
||||
// we need to announce if one list is shorter than the other.
|
||||
if (lastAdded != nil &&
|
||||
([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
|
||||
previous_route_id_ = [lastAdded uid];
|
||||
routeChanged = true;
|
||||
}
|
||||
|
||||
@ -333,6 +333,178 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
|
||||
UIAccessibilityScreenChangedNotification);
|
||||
}
|
||||
|
||||
- (void)testAnnouncesRouteChangesWhenAddAdditionalRoute {
|
||||
flutter::MockDelegate mock_delegate;
|
||||
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
|
||||
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
|
||||
/*platform=*/thread_task_runner,
|
||||
/*raster=*/thread_task_runner,
|
||||
/*ui=*/thread_task_runner,
|
||||
/*io=*/thread_task_runner);
|
||||
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
|
||||
/*delegate=*/mock_delegate,
|
||||
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
|
||||
/*task_runners=*/runners);
|
||||
id mockFlutterView = OCMClassMock([FlutterView class]);
|
||||
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
|
||||
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
|
||||
|
||||
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
|
||||
[[[NSMutableArray alloc] init] autorelease];
|
||||
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
|
||||
ios_delegate->on_PostAccessibilityNotification_ =
|
||||
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
|
||||
[accessibility_notifications addObject:@{
|
||||
@"notification" : @(notification),
|
||||
@"argument" : argument ? argument : [NSNull null],
|
||||
}];
|
||||
};
|
||||
__block auto bridge =
|
||||
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
|
||||
/*platform_view=*/platform_view.get(),
|
||||
/*platform_views_controller=*/nil,
|
||||
/*ios_delegate=*/std::move(ios_delegate));
|
||||
|
||||
flutter::CustomAccessibilityActionUpdates actions;
|
||||
flutter::SemanticsNodeUpdates nodes;
|
||||
|
||||
flutter::SemanticsNode node1;
|
||||
node1.id = 1;
|
||||
node1.label = "node1";
|
||||
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
nodes[node1.id] = node1;
|
||||
flutter::SemanticsNode root_node;
|
||||
root_node.id = kRootNodeId;
|
||||
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
|
||||
root_node.childrenInTraversalOrder = {1};
|
||||
root_node.childrenInHitTestOrder = {1};
|
||||
nodes[root_node.id] = root_node;
|
||||
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
|
||||
|
||||
XCTAssertEqual([accessibility_notifications count], 1ul);
|
||||
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
|
||||
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
|
||||
UIAccessibilityScreenChangedNotification);
|
||||
|
||||
flutter::SemanticsNodeUpdates new_nodes;
|
||||
|
||||
flutter::SemanticsNode new_node1;
|
||||
new_node1.id = 1;
|
||||
new_node1.label = "new_node1";
|
||||
new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
new_node1.childrenInTraversalOrder = {2};
|
||||
new_node1.childrenInHitTestOrder = {2};
|
||||
new_nodes[new_node1.id] = new_node1;
|
||||
flutter::SemanticsNode new_node2;
|
||||
new_node2.id = 2;
|
||||
new_node2.label = "new_node2";
|
||||
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
new_nodes[new_node2.id] = new_node2;
|
||||
flutter::SemanticsNode new_root_node;
|
||||
new_root_node.id = kRootNodeId;
|
||||
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
|
||||
new_root_node.childrenInTraversalOrder = {1};
|
||||
new_root_node.childrenInHitTestOrder = {1};
|
||||
new_nodes[new_root_node.id] = new_root_node;
|
||||
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
|
||||
XCTAssertEqual([accessibility_notifications count], 2ul);
|
||||
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
|
||||
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
|
||||
UIAccessibilityScreenChangedNotification);
|
||||
}
|
||||
|
||||
- (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
|
||||
flutter::MockDelegate mock_delegate;
|
||||
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
|
||||
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
|
||||
/*platform=*/thread_task_runner,
|
||||
/*raster=*/thread_task_runner,
|
||||
/*ui=*/thread_task_runner,
|
||||
/*io=*/thread_task_runner);
|
||||
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
|
||||
/*delegate=*/mock_delegate,
|
||||
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
|
||||
/*task_runners=*/runners);
|
||||
id mockFlutterView = OCMClassMock([FlutterView class]);
|
||||
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
|
||||
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
|
||||
|
||||
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
|
||||
[[[NSMutableArray alloc] init] autorelease];
|
||||
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
|
||||
ios_delegate->on_PostAccessibilityNotification_ =
|
||||
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
|
||||
[accessibility_notifications addObject:@{
|
||||
@"notification" : @(notification),
|
||||
@"argument" : argument ? argument : [NSNull null],
|
||||
}];
|
||||
};
|
||||
__block auto bridge =
|
||||
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
|
||||
/*platform_view=*/platform_view.get(),
|
||||
/*platform_views_controller=*/nil,
|
||||
/*ios_delegate=*/std::move(ios_delegate));
|
||||
|
||||
flutter::CustomAccessibilityActionUpdates actions;
|
||||
flutter::SemanticsNodeUpdates nodes;
|
||||
|
||||
flutter::SemanticsNode node1;
|
||||
node1.id = 1;
|
||||
node1.label = "node1";
|
||||
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
node1.childrenInTraversalOrder = {2};
|
||||
node1.childrenInHitTestOrder = {2};
|
||||
nodes[node1.id] = node1;
|
||||
flutter::SemanticsNode node2;
|
||||
node2.id = 2;
|
||||
node2.label = "node2";
|
||||
node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
nodes[node2.id] = node2;
|
||||
flutter::SemanticsNode root_node;
|
||||
root_node.id = kRootNodeId;
|
||||
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
|
||||
root_node.childrenInTraversalOrder = {1};
|
||||
root_node.childrenInHitTestOrder = {1};
|
||||
nodes[root_node.id] = root_node;
|
||||
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
|
||||
|
||||
XCTAssertEqual([accessibility_notifications count], 1ul);
|
||||
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node2");
|
||||
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
|
||||
UIAccessibilityScreenChangedNotification);
|
||||
|
||||
flutter::SemanticsNodeUpdates new_nodes;
|
||||
|
||||
flutter::SemanticsNode new_node1;
|
||||
new_node1.id = 1;
|
||||
new_node1.label = "new_node1";
|
||||
new_node1.childrenInTraversalOrder = {2};
|
||||
new_node1.childrenInHitTestOrder = {2};
|
||||
new_nodes[new_node1.id] = new_node1;
|
||||
flutter::SemanticsNode new_node2;
|
||||
new_node2.id = 2;
|
||||
new_node2.label = "new_node2";
|
||||
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
|
||||
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
|
||||
new_nodes[new_node2.id] = new_node2;
|
||||
flutter::SemanticsNode new_root_node;
|
||||
new_root_node.id = kRootNodeId;
|
||||
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
|
||||
new_root_node.childrenInTraversalOrder = {1};
|
||||
new_root_node.childrenInHitTestOrder = {1};
|
||||
new_nodes[new_root_node.id] = new_root_node;
|
||||
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
|
||||
XCTAssertEqual([accessibility_notifications count], 2ul);
|
||||
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
|
||||
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
|
||||
UIAccessibilityScreenChangedNotification);
|
||||
}
|
||||
|
||||
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
|
||||
flutter::MockDelegate mock_delegate;
|
||||
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user