@@ -743,10 +743,83 @@ - (void)testLayoutChangeDoesCallNativeAccessibility {
743743 bridge->UpdateSemantics (/* nodes=*/ new_nodes, /* actions=*/ new_actions);
744744
745745 XCTAssertEqual ([accessibility_notifications count ], 1ul );
746- SemanticsObject* focusObject = accessibility_notifications[0 ][@" argument" ];
747- // Make sure refocus event is sent with the nativeAccessibility of root node
748- // which is a FlutterSemanticsScrollView.
749- XCTAssertTrue ([focusObject isKindOfClass: [FlutterSemanticsScrollView class ]]);
746+ id focusObject = accessibility_notifications[0 ][@" argument" ];
747+
748+ // Make sure the focused item is not specificed when it stays the same.
749+ // See: https://github.com/flutter/flutter/issues/104176
750+ XCTAssertEqualObjects (focusObject, [NSNull null ]);
751+ XCTAssertEqual ([accessibility_notifications[0 ][@" notification" ] unsignedIntValue ],
752+ UIAccessibilityLayoutChangedNotification);
753+ }
754+
755+ - (void )testLayoutChangeDoesCallNativeAccessibilityWhenFocusChanged {
756+ flutter::MockDelegate mock_delegate;
757+ auto thread_task_runner = CreateNewThread (" AccessibilityBridgeTest" );
758+ flutter::TaskRunners runners (/* label=*/ self.name .UTF8String ,
759+ /* platform=*/ thread_task_runner,
760+ /* raster=*/ thread_task_runner,
761+ /* ui=*/ thread_task_runner,
762+ /* io=*/ thread_task_runner);
763+ auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
764+ /* delegate=*/ mock_delegate,
765+ /* rendering_api=*/ flutter::IOSRenderingAPI::kSoftware ,
766+ /* platform_views_controller=*/ nil ,
767+ /* task_runners=*/ runners);
768+ id mockFlutterView = OCMClassMock ([FlutterView class ]);
769+ id mockFlutterViewController = OCMClassMock ([FlutterViewController class ]);
770+ OCMStub ([mockFlutterViewController view ]).andReturn (mockFlutterView);
771+
772+ NSMutableArray <NSDictionary <NSString *, id >*>* accessibility_notifications =
773+ [[[NSMutableArray alloc ] init ] autorelease ];
774+ auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
775+ ios_delegate->on_PostAccessibilityNotification_ =
776+ [accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
777+ [accessibility_notifications addObject: @{
778+ @" notification" : @(notification),
779+ @" argument" : argument ? argument : [NSNull null ],
780+ }];
781+ };
782+ __block auto bridge =
783+ std::make_unique<flutter::AccessibilityBridge>(/* view_controller=*/ mockFlutterViewController,
784+ /* platform_view=*/ platform_view.get (),
785+ /* platform_views_controller=*/ nil ,
786+ /* ios_delegate=*/ std::move (ios_delegate));
787+
788+ flutter::CustomAccessibilityActionUpdates actions;
789+ flutter::SemanticsNodeUpdates nodes;
790+
791+ flutter::SemanticsNode node1;
792+ node1.id = 1 ;
793+ node1.label = " node1" ;
794+ nodes[node1.id ] = node1;
795+ flutter::SemanticsNode root_node;
796+ root_node.id = kRootNodeId ;
797+ root_node.label = " root" ;
798+ root_node.flags = static_cast <int32_t >(flutter::SemanticsFlags::kHasImplicitScrolling );
799+ root_node.childrenInTraversalOrder = {1 };
800+ root_node.childrenInHitTestOrder = {1 };
801+ nodes[root_node.id ] = root_node;
802+ bridge->UpdateSemantics (/* nodes=*/ nodes, /* actions=*/ actions);
803+
804+ // Simulates the focusing on the node 1.
805+ bridge->AccessibilityObjectDidBecomeFocused (1 );
806+
807+ // Remove node 1 to trigger a layout change notification, and focus should be one root
808+ flutter::CustomAccessibilityActionUpdates new_actions;
809+ flutter::SemanticsNodeUpdates new_nodes;
810+
811+ flutter::SemanticsNode new_root_node;
812+ new_root_node.id = kRootNodeId ;
813+ new_root_node.label = " root" ;
814+ new_root_node.flags = static_cast <int32_t >(flutter::SemanticsFlags::kHasImplicitScrolling );
815+ new_nodes[new_root_node.id ] = new_root_node;
816+ bridge->UpdateSemantics (/* nodes=*/ new_nodes, /* actions=*/ new_actions);
817+
818+ XCTAssertEqual ([accessibility_notifications count ], 1ul );
819+ SemanticsObject* focusObject2 = accessibility_notifications[0 ][@" argument" ];
820+
821+ // Bridge should ask accessibility to focus on root because node 1 is moved from screen.
822+ XCTAssertTrue ([focusObject2 isKindOfClass: [FlutterSemanticsScrollView class ]]);
750823 XCTAssertEqual ([accessibility_notifications[0 ][@" notification" ] unsignedIntValue ],
751824 UIAccessibilityLayoutChangedNotification);
752825}
@@ -896,7 +969,6 @@ - (void)testAnnouncesRouteChangesAndLayoutChangeInOneUpdate {
896969 XCTAssertEqual ([accessibility_notifications[1 ][@" notification" ] unsignedIntValue ],
897970 UIAccessibilityScreenChangedNotification);
898971 SemanticsObject* focusObject = accessibility_notifications[2 ][@" argument" ];
899- // It should still focus the root.
900972 XCTAssertEqual ([focusObject uid ], 0 );
901973 XCTAssertEqual ([accessibility_notifications[2 ][@" notification" ] unsignedIntValue ],
902974 UIAccessibilityLayoutChangedNotification);
@@ -1211,7 +1283,7 @@ - (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
12111283 UIAccessibilityLayoutChangedNotification);
12121284}
12131285
1214- - (void )testAnnouncesLayoutChangeWithLastFocused {
1286+ - (void )testAnnouncesLayoutChangeWithTheSameItemFocused {
12151287 flutter::MockDelegate mock_delegate;
12161288 auto thread_task_runner = CreateNewThread (" AccessibilityBridgeTest" );
12171289 flutter::TaskRunners runners (/* label=*/ self.name .UTF8String ,
@@ -1276,10 +1348,10 @@ - (void)testAnnouncesLayoutChangeWithLastFocused {
12761348 new_root_node.childrenInHitTestOrder = {1 };
12771349 second_update[root_node.id ] = new_root_node;
12781350 bridge->UpdateSemantics (/* nodes=*/ second_update, /* actions=*/ actions);
1279- SemanticsObject* focusObject = accessibility_notifications[0 ][@" argument" ];
1280- // Since we have focused on the node 1 right before the layout changed, the bridge should refocus
1281- // the node 1 .
1282- XCTAssertEqual ([ focusObject uid ], 1 );
1351+ id focusObject = accessibility_notifications[0 ][@" argument" ];
1352+ // Since we have focused on the node 1 right before the layout changed, the bridge should not ask
1353+ // to refocus again on the same node .
1354+ XCTAssertEqualObjects ( focusObject, [ NSNull null ] );
12831355 XCTAssertEqual ([accessibility_notifications[0 ][@" notification" ] unsignedIntValue ],
12841356 UIAccessibilityLayoutChangedNotification);
12851357}
0 commit comments