@@ -561,6 +561,132 @@ void main() {
561561 expect (focusNode.hasPrimaryFocus, isTrue);
562562 expect (FocusManager .instance.rootScope.hasPrimaryFocus, isFalse);
563563 });
564+
565+ testWidgets ('View notifies engine that a view should have focus when a widget focus change occurs.' , (WidgetTester tester) async {
566+ final FocusNode nodeA = FocusNode (debugLabel: 'a' );
567+ addTearDown (nodeA.dispose);
568+
569+ FlutterView ? view;
570+ await tester.pumpWidget (
571+ Directionality (
572+ textDirection: TextDirection .rtl,
573+ child: Column (
574+ children: < Widget > [
575+ Focus (focusNode: nodeA, child: const Text ('a' )),
576+ Builder (builder: (BuildContext context) {
577+ view = View .of (context);
578+ return const SizedBox .shrink ();
579+ }),
580+ ],
581+ ),
582+ ),
583+ );
584+ int notifyCount = 0 ;
585+ void handleFocusChange () {
586+ notifyCount++ ;
587+ }
588+ tester.binding.focusManager.addListener (handleFocusChange);
589+ addTearDown (() => tester.binding.focusManager.removeListener (handleFocusChange));
590+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
591+
592+ nodeA.requestFocus ();
593+ await tester.pump ();
594+ final List <ViewFocusEvent > events = tester.binding.platformDispatcher.testFocusEvents;
595+ expect (events.length, equals (1 ));
596+ expect (events.last.viewId, equals (view? .viewId));
597+ expect (events.last.direction, equals (ViewFocusDirection .forward));
598+ expect (events.last.state, equals (ViewFocusState .focused));
599+ expect (nodeA.hasPrimaryFocus, isTrue);
600+ expect (notifyCount, equals (1 ));
601+ notifyCount = 0 ;
602+ });
603+
604+ testWidgets ('Switching focus between views yields the correct events.' , (WidgetTester tester) async {
605+ final FocusNode nodeA = FocusNode (debugLabel: 'a' );
606+ addTearDown (nodeA.dispose);
607+
608+ FlutterView ? view;
609+ await tester.pumpWidget (
610+ Directionality (
611+ textDirection: TextDirection .rtl,
612+ child: Column (
613+ children: < Widget > [
614+ Focus (focusNode: nodeA, child: const Text ('a' )),
615+ Builder (builder: (BuildContext context) {
616+ view = View .of (context);
617+ return const SizedBox .shrink ();
618+ }),
619+ ],
620+ ),
621+ ),
622+ );
623+ int notifyCount = 0 ;
624+ void handleFocusChange () {
625+ notifyCount++ ;
626+ }
627+ tester.binding.focusManager.addListener (handleFocusChange);
628+ addTearDown (() => tester.binding.focusManager.removeListener (handleFocusChange));
629+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
630+
631+ // Focus and make sure engine is notified.
632+ nodeA.requestFocus ();
633+ await tester.pump ();
634+ List <ViewFocusEvent > events = tester.binding.platformDispatcher.testFocusEvents;
635+ expect (events.length, equals (1 ));
636+ expect (events.last.viewId, equals (view? .viewId));
637+ expect (events.last.direction, equals (ViewFocusDirection .forward));
638+ expect (events.last.state, equals (ViewFocusState .focused));
639+ expect (nodeA.hasPrimaryFocus, isTrue);
640+ expect (notifyCount, equals (1 ));
641+ notifyCount = 0 ;
642+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
643+
644+ // Unfocus all views.
645+ tester.binding.platformDispatcher.onViewFocusChange? .call (
646+ ViewFocusEvent (
647+ viewId: view! .viewId,
648+ state: ViewFocusState .unfocused,
649+ direction: ViewFocusDirection .forward,
650+ ),
651+ );
652+ await tester.pump ();
653+ expect (nodeA.hasFocus, isFalse);
654+ expect (tester.binding.platformDispatcher.testFocusEvents, isEmpty);
655+ expect (notifyCount, equals (1 ));
656+ notifyCount = 0 ;
657+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
658+
659+ // Focus another view.
660+ tester.binding.platformDispatcher.onViewFocusChange? .call (
661+ const ViewFocusEvent (
662+ viewId: 100 ,
663+ state: ViewFocusState .focused,
664+ direction: ViewFocusDirection .forward,
665+ ),
666+ );
667+
668+ // Focusing another view should unfocus this node without notifying the
669+ // engine to unfocus.
670+ await tester.pump ();
671+ expect (nodeA.hasFocus, isFalse);
672+ expect (tester.binding.platformDispatcher.testFocusEvents, isEmpty);
673+ expect (notifyCount, equals (0 ));
674+ notifyCount = 0 ;
675+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
676+
677+ // Re-focusing the node should notify the engine that this view is focused.
678+ nodeA.requestFocus ();
679+ await tester.pump ();
680+ expect (nodeA.hasPrimaryFocus, isTrue);
681+ events = tester.binding.platformDispatcher.testFocusEvents;
682+ expect (events.length, equals (1 ));
683+ expect (events.last.viewId, equals (view? .viewId));
684+ expect (events.last.direction, equals (ViewFocusDirection .forward));
685+ expect (events.last.state, equals (ViewFocusState .focused));
686+ expect (notifyCount, equals (1 ));
687+ notifyCount = 0 ;
688+ tester.binding.platformDispatcher.resetFocusedViewTestValues ();
689+ });
564690}
565691
566692class SpyRenderWidget extends SizedBox {
0 commit comments