"Fossies" - the Fresh Open Source Software Archive

Member "flutter-3.7.0/packages/flutter/test/widgets/actions_test.dart" (24 Jan 2023, 68947 Bytes) of package /linux/misc/flutter-3.7.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Dart source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "actions_test.dart": 3.3.10_vs_3.7.0.

    1 // Copyright 2014 The Flutter Authors. All rights reserved.
    2 // Use of this source code is governed by a BSD-style license that can be
    3 // found in the LICENSE file.
    4 
    5 import 'package:flutter/foundation.dart';
    6 import 'package:flutter/gestures.dart';
    7 import 'package:flutter/material.dart';
    8 import 'package:flutter/rendering.dart';
    9 import 'package:flutter/services.dart';
   10 import 'package:flutter_test/flutter_test.dart';
   11 
   12 void main() {
   13   group(ActionDispatcher, () {
   14     testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async {
   15       await tester.pumpWidget(Container());
   16       bool invoked = false;
   17       const ActionDispatcher dispatcher = ActionDispatcher();
   18       final Object? result = dispatcher.invokeAction(
   19         TestAction(
   20           onInvoke: (Intent intent) {
   21             invoked = true;
   22             return invoked;
   23           },
   24         ),
   25         const TestIntent(),
   26       );
   27       expect(result, isTrue);
   28       expect(invoked, isTrue);
   29     });
   30   });
   31 
   32   group(Actions, () {
   33     Intent? invokedIntent;
   34     Action<Intent>? invokedAction;
   35     ActionDispatcher? invokedDispatcher;
   36 
   37     void collect({Action<Intent>? action, Intent? intent, ActionDispatcher? dispatcher}) {
   38       invokedIntent = intent;
   39       invokedAction = action;
   40       invokedDispatcher = dispatcher;
   41     }
   42 
   43     void clear() {
   44       invokedIntent = null;
   45       invokedAction = null;
   46       invokedDispatcher = null;
   47     }
   48 
   49     setUp(clear);
   50 
   51     testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
   52       final GlobalKey containerKey = GlobalKey();
   53       bool invoked = false;
   54 
   55       await tester.pumpWidget(
   56         Actions(
   57           actions: <Type, Action<Intent>>{
   58             TestIntent: TestAction(
   59               onInvoke: (Intent intent) {
   60                 invoked = true;
   61                 return invoked;
   62               },
   63             ),
   64           },
   65           child: Container(key: containerKey),
   66         ),
   67       );
   68 
   69       await tester.pump();
   70       final Object? result = Actions.invoke(
   71         containerKey.currentContext!,
   72         const TestIntent(),
   73       );
   74       expect(result, isTrue);
   75       expect(invoked, isTrue);
   76     });
   77     testWidgets('Actions widget can invoke actions with default dispatcher and maybeInvoke', (WidgetTester tester) async {
   78       final GlobalKey containerKey = GlobalKey();
   79       bool invoked = false;
   80 
   81       await tester.pumpWidget(
   82         Actions(
   83           actions: <Type, Action<Intent>>{
   84             TestIntent: TestAction(
   85               onInvoke: (Intent intent) {
   86                 invoked = true;
   87                 return invoked;
   88               },
   89             ),
   90           },
   91           child: Container(key: containerKey),
   92         ),
   93       );
   94 
   95       await tester.pump();
   96       final Object? result = Actions.maybeInvoke(
   97         containerKey.currentContext!,
   98         const TestIntent(),
   99       );
  100       expect(result, isTrue);
  101       expect(invoked, isTrue);
  102     });
  103 
  104     testWidgets('maybeInvoke returns null when no action is found', (WidgetTester tester) async {
  105       final GlobalKey containerKey = GlobalKey();
  106       bool invoked = false;
  107 
  108       await tester.pumpWidget(
  109         Actions(
  110           actions: <Type, Action<Intent>>{
  111             TestIntent: TestAction(
  112               onInvoke: (Intent intent) {
  113                 invoked = true;
  114                 return invoked;
  115               },
  116             ),
  117           },
  118           child: Container(key: containerKey),
  119         ),
  120       );
  121 
  122       await tester.pump();
  123       final Object? result = Actions.maybeInvoke(
  124         containerKey.currentContext!,
  125         const DoNothingIntent(),
  126       );
  127       expect(result, isNull);
  128       expect(invoked, isFalse);
  129     });
  130 
  131     testWidgets('invoke throws when no action is found', (WidgetTester tester) async {
  132       final GlobalKey containerKey = GlobalKey();
  133       bool invoked = false;
  134 
  135       await tester.pumpWidget(
  136         Actions(
  137           actions: <Type, Action<Intent>>{
  138             TestIntent: TestAction(
  139               onInvoke: (Intent intent) {
  140                 invoked = true;
  141                 return invoked;
  142               },
  143             ),
  144           },
  145           child: Container(key: containerKey),
  146         ),
  147       );
  148 
  149       await tester.pump();
  150       final Object? result = Actions.maybeInvoke(
  151         containerKey.currentContext!,
  152         const DoNothingIntent(),
  153       );
  154       expect(result, isNull);
  155       expect(invoked, isFalse);
  156     });
  157 
  158     testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
  159       final GlobalKey containerKey = GlobalKey();
  160       bool invoked = false;
  161       const TestIntent intent = TestIntent();
  162       final Action<Intent> testAction = TestAction(
  163         onInvoke: (Intent intent) {
  164           invoked = true;
  165           return invoked;
  166         },
  167       );
  168 
  169       await tester.pumpWidget(
  170         Actions(
  171           dispatcher: TestDispatcher(postInvoke: collect),
  172           actions: <Type, Action<Intent>>{
  173             TestIntent: testAction,
  174           },
  175           child: Container(key: containerKey),
  176         ),
  177       );
  178 
  179       await tester.pump();
  180       final Object? result = Actions.invoke<TestIntent>(
  181         containerKey.currentContext!,
  182         intent,
  183       );
  184       expect(result, isTrue);
  185       expect(invoked, isTrue);
  186       expect(invokedIntent, equals(intent));
  187     });
  188 
  189     testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
  190       final GlobalKey containerKey = GlobalKey();
  191       bool invoked = false;
  192       const TestIntent intent = TestIntent();
  193       final Action<Intent> testAction = TestAction(
  194         onInvoke: (Intent intent) {
  195           invoked = true;
  196           return invoked;
  197         },
  198       );
  199 
  200       await tester.pumpWidget(
  201         Actions(
  202           dispatcher: TestDispatcher1(postInvoke: collect),
  203           actions: <Type, Action<Intent>>{
  204             TestIntent: testAction,
  205           },
  206           child: Actions(
  207             dispatcher: TestDispatcher(postInvoke: collect),
  208             actions: const <Type, Action<Intent>>{},
  209             child: Container(key: containerKey),
  210           ),
  211         ),
  212       );
  213 
  214       await tester.pump();
  215       final Object? result = Actions.invoke<TestIntent>(
  216         containerKey.currentContext!,
  217         intent,
  218       );
  219       expect(result, isTrue);
  220       expect(invoked, isTrue);
  221       expect(invokedIntent, equals(intent));
  222       expect(invokedAction, equals(testAction));
  223       expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
  224     });
  225 
  226     testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
  227       final GlobalKey containerKey = GlobalKey();
  228       bool invoked = false;
  229       const TestIntent intent = TestIntent();
  230       final Action<Intent> testAction = TestAction(
  231         onInvoke: (Intent intent) {
  232           invoked = true;
  233           return invoked;
  234         },
  235       );
  236 
  237       await tester.pumpWidget(
  238         Actions(
  239           dispatcher: TestDispatcher1(postInvoke: collect),
  240           actions: <Type, Action<Intent>>{
  241             TestIntent: testAction,
  242           },
  243           child: Actions(
  244             actions: const <Type, Action<Intent>>{},
  245             child: Container(key: containerKey),
  246           ),
  247         ),
  248       );
  249 
  250       await tester.pump();
  251       final Object? result = Actions.invoke<TestIntent>(
  252         containerKey.currentContext!,
  253         intent,
  254       );
  255       expect(result, isTrue);
  256       expect(invoked, isTrue);
  257       expect(invokedIntent, equals(intent));
  258       expect(invokedAction, equals(testAction));
  259       expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
  260     });
  261 
  262     testWidgets('Actions widget can be found with of', (WidgetTester tester) async {
  263       final GlobalKey containerKey = GlobalKey();
  264       final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
  265 
  266       await tester.pumpWidget(
  267         Actions(
  268           dispatcher: testDispatcher,
  269           actions: const <Type, Action<Intent>>{},
  270           child: Container(key: containerKey),
  271         ),
  272       );
  273 
  274       await tester.pump();
  275       final ActionDispatcher dispatcher = Actions.of(containerKey.currentContext!);
  276       expect(dispatcher, equals(testDispatcher));
  277     });
  278 
  279     testWidgets('Action can be found with find', (WidgetTester tester) async {
  280       final GlobalKey containerKey = GlobalKey();
  281       final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
  282       bool invoked = false;
  283       final TestAction testAction = TestAction(
  284         onInvoke: (Intent intent) {
  285           invoked = true;
  286           return invoked;
  287         },
  288       );
  289       await tester.pumpWidget(
  290         Actions(
  291           dispatcher: testDispatcher,
  292           actions: <Type, Action<Intent>>{
  293             TestIntent: testAction,
  294           },
  295           child: Actions(
  296             actions: const <Type, Action<Intent>>{},
  297             child: Container(key: containerKey),
  298           ),
  299         ),
  300       );
  301 
  302       await tester.pump();
  303       expect(Actions.find<TestIntent>(containerKey.currentContext!), equals(testAction));
  304       expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext!), throwsAssertionError);
  305       expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull);
  306 
  307       await tester.pumpWidget(
  308         Actions(
  309           dispatcher: testDispatcher,
  310           actions: <Type, Action<Intent>>{
  311             TestIntent: testAction,
  312           },
  313           child: Actions(
  314             actions: const <Type, Action<Intent>>{},
  315             child: Container(key: containerKey),
  316           ),
  317         ),
  318       );
  319 
  320       await tester.pump();
  321       expect(Actions.find<TestIntent>(containerKey.currentContext!), equals(testAction));
  322       expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext!), throwsAssertionError);
  323       expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull);
  324     });
  325 
  326     testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
  327       FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
  328       final GlobalKey containerKey = GlobalKey();
  329       bool invoked = false;
  330       const Intent intent = TestIntent();
  331       final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
  332       final Action<Intent> testAction = TestAction(
  333         onInvoke: (Intent intent) {
  334           invoked = true;
  335           return invoked;
  336         },
  337       );
  338       bool hovering = false;
  339       bool focusing = false;
  340 
  341       Future<void> buildTest(bool enabled) async {
  342         await tester.pumpWidget(
  343           Center(
  344             child: Actions(
  345               dispatcher: TestDispatcher1(postInvoke: collect),
  346               actions: const <Type, Action<Intent>>{},
  347               child: FocusableActionDetector(
  348                 enabled: enabled,
  349                 focusNode: focusNode,
  350                 shortcuts: const <ShortcutActivator, Intent>{
  351                   SingleActivator(LogicalKeyboardKey.enter): intent,
  352                 },
  353                 actions: <Type, Action<Intent>>{
  354                   TestIntent: testAction,
  355                 },
  356                 onShowHoverHighlight: (bool value) => hovering = value,
  357                 onShowFocusHighlight: (bool value) => focusing = value,
  358                 child: SizedBox(width: 100, height: 100, key: containerKey),
  359               ),
  360             ),
  361           ),
  362         );
  363         return tester.pump();
  364       }
  365 
  366       await buildTest(true);
  367       focusNode.requestFocus();
  368       await tester.pump();
  369       final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
  370       await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
  371       await tester.pump();
  372       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  373       expect(hovering, isTrue);
  374       expect(focusing, isTrue);
  375       expect(invoked, isTrue);
  376 
  377       invoked = false;
  378       await buildTest(false);
  379       expect(hovering, isFalse);
  380       expect(focusing, isFalse);
  381       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  382       await tester.pump();
  383       expect(invoked, isFalse);
  384       await buildTest(true);
  385       expect(focusing, isFalse);
  386       expect(hovering, isTrue);
  387       await buildTest(false);
  388       expect(focusing, isFalse);
  389       expect(hovering, isFalse);
  390       await gesture.moveTo(Offset.zero);
  391       await buildTest(true);
  392       expect(hovering, isFalse);
  393       expect(focusing, isFalse);
  394     });
  395 
  396     testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async {
  397       await tester.pumpWidget(
  398         MouseRegion(
  399           cursor: SystemMouseCursors.forbidden,
  400           child: FocusableActionDetector(
  401             mouseCursor: SystemMouseCursors.text,
  402             onShowHoverHighlight: (_) {},
  403             onShowFocusHighlight: (_) {},
  404             child: Container(),
  405           ),
  406         ),
  407       );
  408       final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
  409       await gesture.addPointer(location: const Offset(1, 1));
  410       await tester.pump();
  411 
  412       expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
  413 
  414       // Test default
  415       await tester.pumpWidget(
  416         MouseRegion(
  417           cursor: SystemMouseCursors.forbidden,
  418           child: FocusableActionDetector(
  419             onShowHoverHighlight: (_) {},
  420             onShowFocusHighlight: (_) {},
  421             child: Container(),
  422           ),
  423         ),
  424       );
  425 
  426       expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
  427     });
  428 
  429     testWidgets('Actions.invoke returns the value of Action.invoke', (WidgetTester tester) async {
  430       final GlobalKey containerKey = GlobalKey();
  431       final Object sentinel = Object();
  432       bool invoked = false;
  433       const TestIntent intent = TestIntent();
  434       final Action<Intent> testAction = TestAction(
  435         onInvoke: (Intent intent) {
  436           invoked = true;
  437           return sentinel;
  438         },
  439       );
  440 
  441       await tester.pumpWidget(
  442         Actions(
  443           dispatcher: TestDispatcher(postInvoke: collect),
  444           actions: <Type, Action<Intent>>{
  445             TestIntent: testAction,
  446           },
  447           child: Container(key: containerKey),
  448         ),
  449       );
  450 
  451       await tester.pump();
  452       final Object? result = Actions.invoke<TestIntent>(
  453         containerKey.currentContext!,
  454         intent,
  455       );
  456       expect(identical(result, sentinel), isTrue);
  457       expect(invoked, isTrue);
  458     });
  459 
  460     testWidgets('ContextAction can return null', (WidgetTester tester) async {
  461       final GlobalKey containerKey = GlobalKey();
  462       const TestIntent intent = TestIntent();
  463       final TestContextAction testAction = TestContextAction();
  464 
  465       await tester.pumpWidget(
  466         Actions(
  467           dispatcher: TestDispatcher1(postInvoke: collect),
  468           actions: <Type, Action<Intent>>{
  469             TestIntent: testAction,
  470           },
  471           child: Container(key: containerKey),
  472         ),
  473       );
  474 
  475       await tester.pump();
  476       final Object? result = Actions.invoke<TestIntent>(
  477         containerKey.currentContext!,
  478         intent,
  479       );
  480       expect(result, isNull);
  481       expect(invokedIntent, equals(intent));
  482       expect(invokedAction, equals(testAction));
  483       expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
  484       expect(testAction.capturedContexts.single, containerKey.currentContext);
  485     });
  486 
  487     testWidgets('Disabled actions stop propagation to an ancestor', (WidgetTester tester) async {
  488       final GlobalKey containerKey = GlobalKey();
  489       bool invoked = false;
  490       const TestIntent intent = TestIntent();
  491       final TestAction enabledTestAction = TestAction(
  492         onInvoke: (Intent intent) {
  493           invoked = true;
  494           return invoked;
  495         },
  496       );
  497       enabledTestAction.enabled = true;
  498       final TestAction disabledTestAction = TestAction(
  499         onInvoke: (Intent intent) {
  500           invoked = true;
  501           return invoked;
  502         },
  503       );
  504       disabledTestAction.enabled = false;
  505 
  506       await tester.pumpWidget(
  507         Actions(
  508           dispatcher: TestDispatcher1(postInvoke: collect),
  509           actions: <Type, Action<Intent>>{
  510             TestIntent: enabledTestAction,
  511           },
  512           child: Actions(
  513             dispatcher: TestDispatcher(postInvoke: collect),
  514             actions: <Type, Action<Intent>>{
  515               TestIntent: disabledTestAction,
  516             },
  517             child: Container(key: containerKey),
  518           ),
  519         ),
  520       );
  521 
  522       await tester.pump();
  523       final Object? result = Actions.invoke<TestIntent>(
  524         containerKey.currentContext!,
  525         intent,
  526       );
  527       expect(result, isNull);
  528       expect(invoked, isFalse);
  529       expect(invokedIntent, isNull);
  530       expect(invokedAction, isNull);
  531       expect(invokedDispatcher, isNull);
  532     });
  533   });
  534 
  535   group('Listening', () {
  536     testWidgets('can listen to enabled state of Actions', (WidgetTester tester) async {
  537       final GlobalKey containerKey = GlobalKey();
  538       bool invoked1 = false;
  539       bool invoked2 = false;
  540       bool invoked3 = false;
  541       final TestAction action1 = TestAction(
  542         onInvoke: (Intent intent) {
  543           invoked1 = true;
  544           return invoked1;
  545         },
  546       );
  547       final TestAction action2 = TestAction(
  548         onInvoke: (Intent intent) {
  549           invoked2 = true;
  550           return invoked2;
  551         },
  552       );
  553       final TestAction action3 = TestAction(
  554         onInvoke: (Intent intent) {
  555           invoked3 = true;
  556           return invoked3;
  557         },
  558       );
  559       bool enabled1 = true;
  560       action1.addActionListener((Action<Intent> action) => enabled1 = action.isEnabled(const TestIntent()));
  561       action1.enabled = false;
  562       expect(enabled1, isFalse);
  563 
  564       bool enabled2 = true;
  565       action2.addActionListener((Action<Intent> action) => enabled2 = action.isEnabled(const SecondTestIntent()));
  566       action2.enabled = false;
  567       expect(enabled2, isFalse);
  568 
  569       bool enabled3 = true;
  570       action3.addActionListener((Action<Intent> action) => enabled3 = action.isEnabled(const ThirdTestIntent()));
  571       action3.enabled = false;
  572       expect(enabled3, isFalse);
  573 
  574       await tester.pumpWidget(
  575         Actions(
  576           actions: <Type, Action<TestIntent>>{
  577             TestIntent: action1,
  578             SecondTestIntent: action2,
  579           },
  580           child: Actions(
  581             actions: <Type, Action<TestIntent>>{
  582               ThirdTestIntent: action3,
  583             },
  584             child: Container(key: containerKey),
  585           ),
  586         ),
  587       );
  588 
  589       Object? result = Actions.maybeInvoke(
  590         containerKey.currentContext!,
  591         const TestIntent(),
  592       );
  593       expect(enabled1, isFalse);
  594       expect(result, isNull);
  595       expect(invoked1, isFalse);
  596 
  597       action1.enabled = true;
  598       result = Actions.invoke(
  599         containerKey.currentContext!,
  600         const TestIntent(),
  601       );
  602       expect(enabled1, isTrue);
  603       expect(result, isTrue);
  604       expect(invoked1, isTrue);
  605 
  606       bool? enabledChanged;
  607       await tester.pumpWidget(
  608         Actions(
  609           actions: <Type, Action<Intent>>{
  610             TestIntent: action1,
  611             SecondTestIntent: action2,
  612           },
  613           child: ActionListener(
  614             listener: (Action<Intent> action) => enabledChanged = action.isEnabled(const ThirdTestIntent()),
  615             action: action2,
  616             child: Actions(
  617               actions: <Type, Action<Intent>>{
  618                 ThirdTestIntent: action3,
  619               },
  620               child: Container(key: containerKey),
  621             ),
  622           ),
  623         ),
  624       );
  625 
  626       await tester.pump();
  627       result = Actions.maybeInvoke<TestIntent>(
  628         containerKey.currentContext!,
  629         const SecondTestIntent(),
  630       );
  631       expect(enabledChanged, isNull);
  632       expect(enabled2, isFalse);
  633       expect(result, isNull);
  634       expect(invoked2, isFalse);
  635 
  636       action2.enabled = true;
  637       expect(enabledChanged, isTrue);
  638       result = Actions.invoke<TestIntent>(
  639         containerKey.currentContext!,
  640         const SecondTestIntent(),
  641       );
  642       expect(enabled2, isTrue);
  643       expect(result, isTrue);
  644       expect(invoked2, isTrue);
  645 
  646       await tester.pumpWidget(
  647         Actions(
  648           actions: <Type, Action<Intent>>{
  649             TestIntent: action1,
  650           },
  651           child: Actions(
  652             actions: <Type, Action<Intent>>{
  653               ThirdTestIntent: action3,
  654             },
  655             child: Container(key: containerKey),
  656           ),
  657         ),
  658       );
  659 
  660       expect(action1.listeners.length, equals(2));
  661       expect(action2.listeners.length, equals(1));
  662       expect(action3.listeners.length, equals(2));
  663 
  664       await tester.pumpWidget(
  665         Actions(
  666           actions: <Type, Action<Intent>>{
  667             TestIntent: action1,
  668             ThirdTestIntent: action3,
  669           },
  670           child: Container(key: containerKey),
  671         ),
  672       );
  673 
  674       expect(action1.listeners.length, equals(2));
  675       expect(action2.listeners.length, equals(1));
  676       expect(action3.listeners.length, equals(2));
  677 
  678       await tester.pumpWidget(
  679         Actions(
  680           actions: <Type, Action<Intent>>{
  681             TestIntent: action1,
  682           },
  683           child: Container(key: containerKey),
  684         ),
  685       );
  686 
  687       expect(action1.listeners.length, equals(2));
  688       expect(action2.listeners.length, equals(1));
  689       expect(action3.listeners.length, equals(1));
  690 
  691       await tester.pumpWidget(Container());
  692       await tester.pump();
  693 
  694       expect(action1.listeners.length, equals(1));
  695       expect(action2.listeners.length, equals(1));
  696       expect(action3.listeners.length, equals(1));
  697     });
  698   });
  699 
  700   group(FocusableActionDetector, () {
  701     const Intent intent = TestIntent();
  702     late bool invoked;
  703     late bool hovering;
  704     late bool focusing;
  705     late FocusNode focusNode;
  706     late Action<Intent> testAction;
  707 
  708     Future<void> pumpTest(
  709         WidgetTester tester, {
  710           bool enabled = true,
  711           bool directional = false,
  712           bool supplyCallbacks = true,
  713           required Key key,
  714         }) async {
  715       await tester.pumpWidget(
  716         MediaQuery(
  717           data: MediaQueryData(
  718             navigationMode: directional ? NavigationMode.directional : NavigationMode.traditional,
  719           ),
  720           child: Center(
  721             child: Actions(
  722               dispatcher: const TestDispatcher1(),
  723               actions: const <Type, Action<Intent>>{},
  724               child: FocusableActionDetector(
  725                 enabled: enabled,
  726                 focusNode: focusNode,
  727                 shortcuts: const <ShortcutActivator, Intent>{
  728                   SingleActivator(LogicalKeyboardKey.enter): intent,
  729                 },
  730                 actions: <Type, Action<Intent>>{
  731                   TestIntent: testAction,
  732                 },
  733                 onShowHoverHighlight: supplyCallbacks ? (bool value) => hovering = value : null,
  734                 onShowFocusHighlight: supplyCallbacks ? (bool value) => focusing = value : null,
  735                 child: SizedBox(width: 100, height: 100, key: key),
  736               ),
  737             ),
  738           ),
  739         ),
  740       );
  741       return tester.pump();
  742     }
  743 
  744     setUp(() async {
  745       invoked = false;
  746       hovering = false;
  747       focusing = false;
  748 
  749       focusNode = FocusNode(debugLabel: 'Test Node');
  750       testAction = TestAction(
  751         onInvoke: (Intent intent) {
  752           invoked = true;
  753           return invoked;
  754         },
  755       );
  756     });
  757 
  758     testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
  759       FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
  760       final GlobalKey containerKey = GlobalKey();
  761 
  762       await pumpTest(tester, key: containerKey);
  763       focusNode.requestFocus();
  764       await tester.pump();
  765       final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
  766       await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
  767       await tester.pump();
  768       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  769       expect(hovering, isTrue);
  770       expect(focusing, isTrue);
  771       expect(invoked, isTrue);
  772 
  773       invoked = false;
  774       await pumpTest(tester, enabled: false, key: containerKey);
  775       expect(hovering, isFalse);
  776       expect(focusing, isFalse);
  777       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  778       await tester.pump();
  779       expect(invoked, isFalse);
  780       await pumpTest(tester, key: containerKey);
  781       expect(focusing, isFalse);
  782       expect(hovering, isTrue);
  783       await pumpTest(tester, enabled: false, key: containerKey);
  784       expect(focusing, isFalse);
  785       expect(hovering, isFalse);
  786       await gesture.moveTo(Offset.zero);
  787       await pumpTest(tester, key: containerKey);
  788       expect(hovering, isFalse);
  789       expect(focusing, isFalse);
  790     });
  791 
  792     testWidgets('FocusableActionDetector shows focus highlight appropriately when focused and disabled', (WidgetTester tester) async {
  793       FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
  794       final GlobalKey containerKey = GlobalKey();
  795 
  796       await pumpTest(tester, key: containerKey);
  797       await tester.pump();
  798       expect(focusing, isFalse);
  799 
  800       await pumpTest(tester, key: containerKey);
  801       focusNode.requestFocus();
  802       await tester.pump();
  803       expect(focusing, isTrue);
  804 
  805       focusing = false;
  806       await pumpTest(tester, enabled: false, key: containerKey);
  807       focusNode.requestFocus();
  808       await tester.pump();
  809       expect(focusing, isFalse);
  810 
  811       await pumpTest(tester, enabled: false, key: containerKey);
  812       focusNode.requestFocus();
  813       await tester.pump();
  814       expect(focusing, isFalse);
  815 
  816       // In directional navigation, focus should show, even if disabled.
  817       await pumpTest(tester, enabled: false, key: containerKey, directional: true);
  818       focusNode.requestFocus();
  819       await tester.pump();
  820       expect(focusing, isTrue);
  821     });
  822 
  823     testWidgets('FocusableActionDetector can be used without callbacks', (WidgetTester tester) async {
  824       FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
  825       final GlobalKey containerKey = GlobalKey();
  826 
  827       await pumpTest(tester, key: containerKey, supplyCallbacks: false);
  828       focusNode.requestFocus();
  829       await tester.pump();
  830       final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
  831       await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
  832       await tester.pump();
  833       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  834       expect(hovering, isFalse);
  835       expect(focusing, isFalse);
  836       expect(invoked, isTrue);
  837 
  838       invoked = false;
  839       await pumpTest(tester, enabled: false, key: containerKey, supplyCallbacks: false);
  840       expect(hovering, isFalse);
  841       expect(focusing, isFalse);
  842       await tester.sendKeyEvent(LogicalKeyboardKey.enter);
  843       await tester.pump();
  844       expect(invoked, isFalse);
  845       await pumpTest(tester, key: containerKey, supplyCallbacks: false);
  846       expect(focusing, isFalse);
  847       expect(hovering, isFalse);
  848       await pumpTest(tester, enabled: false, key: containerKey, supplyCallbacks: false);
  849       expect(focusing, isFalse);
  850       expect(hovering, isFalse);
  851       await gesture.moveTo(Offset.zero);
  852       await pumpTest(tester, key: containerKey, supplyCallbacks: false);
  853       expect(hovering, isFalse);
  854       expect(focusing, isFalse);
  855     });
  856 
  857     testWidgets(
  858       'FocusableActionDetector can prevent its descendants from being focusable',
  859       (WidgetTester tester) async {
  860         final FocusNode buttonNode = FocusNode(debugLabel: 'Test');
  861 
  862         await tester.pumpWidget(
  863           MaterialApp(
  864             home: FocusableActionDetector(
  865               child: MaterialButton(
  866                 focusNode: buttonNode,
  867                 child: const Text('Test'),
  868                 onPressed: () {},
  869               ),
  870             ),
  871           ),
  872         );
  873 
  874         // Button is focusable
  875         expect(buttonNode.hasFocus, isFalse);
  876         buttonNode.requestFocus();
  877         await tester.pump();
  878         expect(buttonNode.hasFocus, isTrue);
  879 
  880         await tester.pumpWidget(
  881           MaterialApp(
  882             home: FocusableActionDetector(
  883               descendantsAreFocusable: false,
  884               child: MaterialButton(
  885                 focusNode: buttonNode,
  886                 child: const Text('Test'),
  887                 onPressed: () {},
  888               ),
  889             ),
  890           ),
  891         );
  892 
  893         // Button is NOT focusable
  894         expect(buttonNode.hasFocus, isFalse);
  895         buttonNode.requestFocus();
  896         await tester.pump();
  897         expect(buttonNode.hasFocus, isFalse);
  898       },
  899     );
  900 
  901     testWidgets(
  902       'FocusableActionDetector can prevent its descendants from being traversable',
  903           (WidgetTester tester) async {
  904         final FocusNode buttonNode1 = FocusNode(debugLabel: 'Button Node 1');
  905         final FocusNode buttonNode2 = FocusNode(debugLabel: 'Button Node 2');
  906 
  907         await tester.pumpWidget(
  908           MaterialApp(
  909             home: FocusableActionDetector(
  910               child: Column(
  911                 children: <Widget>[
  912                   MaterialButton(
  913                     focusNode: buttonNode1,
  914                     child: const Text('Node 1'),
  915                     onPressed: () {},
  916                   ),
  917                   MaterialButton(
  918                     focusNode: buttonNode2,
  919                     child: const Text('Node 2'),
  920                     onPressed: () {},
  921                   ),
  922                 ],
  923               ),
  924             ),
  925           ),
  926         );
  927 
  928         buttonNode1.requestFocus();
  929         await tester.pump();
  930         expect(buttonNode1.hasFocus, isTrue);
  931         expect(buttonNode2.hasFocus, isFalse);
  932         primaryFocus!.nextFocus();
  933         await tester.pump();
  934         expect(buttonNode1.hasFocus, isFalse);
  935         expect(buttonNode2.hasFocus, isTrue);
  936 
  937         await tester.pumpWidget(
  938           MaterialApp(
  939             home: FocusableActionDetector(
  940               descendantsAreTraversable: false,
  941               child: Column(
  942                 children: <Widget>[
  943                   MaterialButton(
  944                     focusNode: buttonNode1,
  945                     child: const Text('Node 1'),
  946                     onPressed: () {},
  947                   ),
  948                   MaterialButton(
  949                     focusNode: buttonNode2,
  950                     child: const Text('Node 2'),
  951                     onPressed: () {},
  952                   ),
  953                 ],
  954               ),
  955             ),
  956           ),
  957         );
  958 
  959         buttonNode1.requestFocus();
  960         await tester.pump();
  961         expect(buttonNode1.hasFocus, isTrue);
  962         expect(buttonNode2.hasFocus, isFalse);
  963         primaryFocus!.nextFocus();
  964         await tester.pump();
  965         expect(buttonNode1.hasFocus, isTrue);
  966         expect(buttonNode2.hasFocus, isFalse);
  967       },
  968     );
  969 
  970     testWidgets('FocusableActionDetector can exclude Focus semantics', (WidgetTester tester) async {
  971       await tester.pumpWidget(
  972         MaterialApp(
  973           home: FocusableActionDetector(
  974             child: Column(
  975               children: <Widget>[
  976                 TextButton(
  977                   onPressed: () {},
  978                   child: const Text('Button 1'),
  979                 ),
  980                 TextButton(
  981                   onPressed: () {},
  982                   child: const Text('Button 2'),
  983                 ),
  984               ],
  985             ),
  986           ),
  987         ),
  988       );
  989 
  990       expect(
  991         tester.getSemantics(find.byType(FocusableActionDetector)),
  992         matchesSemantics(
  993           scopesRoute: true,
  994           children: <Matcher>[
  995             // This semantic is from `Focus` widget under `FocusableActionDetector`.
  996             matchesSemantics(
  997               isFocusable: true,
  998               children: <Matcher>[
  999                 matchesSemantics(
 1000                   hasTapAction: true,
 1001                   isButton: true,
 1002                   hasEnabledState: true,
 1003                   isEnabled: true,
 1004                   isFocusable: true,
 1005                   label: 'Button 1',
 1006                   textDirection: TextDirection.ltr,
 1007                 ),
 1008                 matchesSemantics(
 1009                   hasTapAction: true,
 1010                   isButton: true,
 1011                   hasEnabledState: true,
 1012                   isEnabled: true,
 1013                   isFocusable: true,
 1014                   label: 'Button 2',
 1015                   textDirection: TextDirection.ltr,
 1016                 ),
 1017               ],
 1018             ),
 1019           ],
 1020         ),
 1021       );
 1022 
 1023       // Set `includeFocusSemantics` to false to exclude semantics
 1024       // from `Focus` widget under `FocusableActionDetector`.
 1025       await tester.pumpWidget(
 1026         MaterialApp(
 1027           home: FocusableActionDetector(
 1028             includeFocusSemantics: false,
 1029             child: Column(
 1030               children: <Widget>[
 1031                 TextButton(
 1032                   onPressed: () {},
 1033                   child: const Text('Button 1'),
 1034                 ),
 1035                 TextButton(
 1036                   onPressed: () {},
 1037                   child: const Text('Button 2'),
 1038                 ),
 1039               ],
 1040             ),
 1041           ),
 1042         ),
 1043       );
 1044 
 1045       // Semantics from the `Focus` widget will be removed.
 1046       expect(
 1047         tester.getSemantics(find.byType(FocusableActionDetector)),
 1048         matchesSemantics(
 1049           scopesRoute: true,
 1050           children: <Matcher>[
 1051             matchesSemantics(
 1052               hasTapAction: true,
 1053               isButton: true,
 1054               hasEnabledState: true,
 1055               isEnabled: true,
 1056               isFocusable: true,
 1057               label: 'Button 1',
 1058               textDirection: TextDirection.ltr,
 1059             ),
 1060             matchesSemantics(
 1061               hasTapAction: true,
 1062               isButton: true,
 1063               hasEnabledState: true,
 1064               isEnabled: true,
 1065               isFocusable: true,
 1066               label: 'Button 2',
 1067               textDirection: TextDirection.ltr,
 1068             ),
 1069           ],
 1070         ),
 1071       );
 1072     });
 1073   });
 1074 
 1075   group('Action subclasses', () {
 1076     testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async {
 1077       late Intent passedIntent;
 1078       final TestAction action = TestAction(onInvoke: (Intent intent) {
 1079         passedIntent = intent;
 1080         return true;
 1081       });
 1082       const TestIntent intent = TestIntent();
 1083       action._testInvoke(intent);
 1084       expect(passedIntent, equals(intent));
 1085     });
 1086     testWidgets('VoidCallbackAction', (WidgetTester tester) async {
 1087       bool called = false;
 1088       void testCallback() {
 1089         called = true;
 1090       }
 1091       final VoidCallbackAction action = VoidCallbackAction();
 1092       final VoidCallbackIntent intent = VoidCallbackIntent(testCallback);
 1093       action.invoke(intent);
 1094       expect(called, isTrue);
 1095     });
 1096   });
 1097 
 1098   group('Diagnostics', () {
 1099     testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
 1100       final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 1101 
 1102       // ignore: invalid_use_of_protected_member
 1103       const TestIntent().debugFillProperties(builder);
 1104 
 1105       final List<String> description = builder.properties
 1106         .where((DiagnosticsNode node) {
 1107           return !node.isFiltered(DiagnosticLevel.info);
 1108         })
 1109         .map((DiagnosticsNode node) => node.toString())
 1110         .toList();
 1111 
 1112       expect(description, isEmpty);
 1113     });
 1114     testWidgets('default Actions debugFillProperties', (WidgetTester tester) async {
 1115       final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 1116 
 1117       Actions(
 1118         actions: const <Type, Action<Intent>>{},
 1119         dispatcher: const ActionDispatcher(),
 1120         child: Container(),
 1121       ).debugFillProperties(builder);
 1122 
 1123       final List<String> description = builder.properties
 1124         .where((DiagnosticsNode node) {
 1125           return !node.isFiltered(DiagnosticLevel.info);
 1126         })
 1127         .map((DiagnosticsNode node) => node.toString())
 1128         .toList();
 1129 
 1130       expect(description.length, equals(2));
 1131       expect(
 1132         description,
 1133         equalsIgnoringHashCodes(<String>[
 1134           'dispatcher: ActionDispatcher#00000',
 1135           'actions: {}',
 1136         ]),
 1137       );
 1138     });
 1139     testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async {
 1140       final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
 1141 
 1142       Actions(
 1143         key: const ValueKey<String>('foo'),
 1144         dispatcher: const ActionDispatcher(),
 1145         actions: <Type, Action<Intent>>{
 1146           TestIntent: TestAction(onInvoke: (Intent intent) => null),
 1147         },
 1148         child: Container(key: const ValueKey<String>('baz')),
 1149       ).debugFillProperties(builder);
 1150 
 1151       final List<String> description = builder.properties
 1152           .where((DiagnosticsNode node) {
 1153             return !node.isFiltered(DiagnosticLevel.info);
 1154           })
 1155           .map((DiagnosticsNode node) => node.toString())
 1156           .toList();
 1157 
 1158       expect(description.length, equals(2));
 1159       expect(
 1160         description,
 1161         equalsIgnoringHashCodes(<String>[
 1162           'dispatcher: ActionDispatcher#00000',
 1163           'actions: {TestIntent: TestAction#00000}',
 1164         ]),
 1165       );
 1166     });
 1167   });
 1168 
 1169   group('Action overriding', () {
 1170     final List<String> invocations = <String>[];
 1171     BuildContext? invokingContext;
 1172 
 1173     tearDown(() {
 1174       invocations.clear();
 1175       invokingContext = null;
 1176     });
 1177 
 1178     testWidgets('Basic usage', (WidgetTester tester) async {
 1179       late BuildContext invokingContext2;
 1180       late BuildContext invokingContext3;
 1181       await tester.pumpWidget(
 1182         Builder(
 1183           builder: (BuildContext context1) {
 1184             return Actions(
 1185               actions: <Type, Action<Intent>> {
 1186                 LogIntent : Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1187               },
 1188               child: Builder(
 1189                 builder: (BuildContext context2) {
 1190                   invokingContext2 = context2;
 1191                   return Actions(
 1192                     actions: <Type, Action<Intent>> {
 1193                       LogIntent : Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2),
 1194                     },
 1195                     child: Builder(
 1196                       builder: (BuildContext context3) {
 1197                         invokingContext3 = context3;
 1198                         return Actions(
 1199                           actions: <Type, Action<Intent>> {
 1200                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1201                           },
 1202                           child: Builder(
 1203                             builder: (BuildContext context4) {
 1204                               invokingContext = context4;
 1205                               return const SizedBox();
 1206                             },
 1207                           ),
 1208                         );
 1209                       },
 1210                     ),
 1211                   );
 1212                 },
 1213               ),
 1214             );
 1215           },
 1216         ),
 1217       );
 1218 
 1219       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1220       expect(invocations, <String>[
 1221         'action1.invokeAsOverride-pre-super',
 1222         'action2.invokeAsOverride-pre-super',
 1223         'action3.invoke',
 1224         'action2.invokeAsOverride-post-super',
 1225         'action1.invokeAsOverride-post-super',
 1226       ]);
 1227 
 1228       invocations.clear();
 1229       // Invoke from a different (higher) context.
 1230       Actions.invoke(invokingContext3, LogIntent(log: invocations));
 1231       expect(invocations, <String>[
 1232         'action1.invokeAsOverride-pre-super',
 1233         'action2.invoke',
 1234         'action1.invokeAsOverride-post-super',
 1235       ]);
 1236 
 1237       invocations.clear();
 1238       // Invoke from a different (higher) context.
 1239       Actions.invoke(invokingContext2, LogIntent(log: invocations));
 1240       expect(invocations, <String>['action1.invoke']);
 1241     });
 1242 
 1243     testWidgets('Does not break after use', (WidgetTester tester) async {
 1244       late BuildContext invokingContext2;
 1245       late BuildContext invokingContext3;
 1246       await tester.pumpWidget(
 1247         Builder(
 1248           builder: (BuildContext context1) {
 1249             return Actions(
 1250               actions: <Type, Action<Intent>> {
 1251                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1252               },
 1253               child: Builder(
 1254                 builder: (BuildContext context2) {
 1255                   invokingContext2 = context2;
 1256                   return Actions(
 1257                     actions: <Type, Action<Intent>> {
 1258                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2),
 1259                     },
 1260                     child: Builder(
 1261                       builder: (BuildContext context3) {
 1262                         invokingContext3 = context3;
 1263                         return Actions(
 1264                           actions: <Type, Action<Intent>> {
 1265                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1266                           },
 1267                           child: Builder(
 1268                             builder: (BuildContext context4) {
 1269                               invokingContext = context4;
 1270                               return const SizedBox();
 1271                             },
 1272                           ),
 1273                         );
 1274                       },
 1275                     ),
 1276                   );
 1277                 },
 1278               ),
 1279             );
 1280           },
 1281         ),
 1282       );
 1283 
 1284       // Invoke a bunch of times and verify it still produces the same result.
 1285       final List<BuildContext> randomContexts = <BuildContext>[
 1286         invokingContext!,
 1287         invokingContext2,
 1288         invokingContext!,
 1289         invokingContext3,
 1290         invokingContext3,
 1291         invokingContext3,
 1292         invokingContext2,
 1293       ];
 1294 
 1295       for (final BuildContext randomContext in randomContexts) {
 1296         Actions.invoke(randomContext, LogIntent(log: invocations));
 1297       }
 1298 
 1299       invocations.clear();
 1300       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1301       expect(invocations, <String>[
 1302         'action1.invokeAsOverride-pre-super',
 1303         'action2.invokeAsOverride-pre-super',
 1304         'action3.invoke',
 1305         'action2.invokeAsOverride-post-super',
 1306         'action1.invokeAsOverride-post-super',
 1307       ]);
 1308     });
 1309 
 1310     testWidgets('Does not override if not overridable', (WidgetTester tester) async {
 1311       await tester.pumpWidget(
 1312         Builder(
 1313           builder: (BuildContext context1) {
 1314             return Actions(
 1315               actions: <Type, Action<Intent>> {
 1316                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1317               },
 1318               child: Builder(
 1319                 builder: (BuildContext context2) {
 1320                   return Actions(
 1321                     actions: <Type, Action<Intent>> { LogIntent : LogInvocationAction(actionName: 'action2') },
 1322                     child: Builder(
 1323                       builder: (BuildContext context3) {
 1324                         return Actions(
 1325                           actions: <Type, Action<Intent>> {
 1326                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1327                           },
 1328                           child: Builder(
 1329                             builder: (BuildContext context4) {
 1330                               invokingContext = context4;
 1331                               return const SizedBox();
 1332                             },
 1333                           ),
 1334                         );
 1335                       },
 1336                     ),
 1337                   );
 1338                 },
 1339               ),
 1340             );
 1341           },
 1342         ),
 1343       );
 1344 
 1345       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1346       expect(invocations, <String>[
 1347         'action2.invokeAsOverride-pre-super',
 1348         'action3.invoke',
 1349         'action2.invokeAsOverride-post-super',
 1350       ]);
 1351     });
 1352 
 1353     testWidgets('The final override controls isEnabled', (WidgetTester tester) async {
 1354       await tester.pumpWidget(
 1355         Builder(
 1356           builder: (BuildContext context1) {
 1357             return Actions(
 1358               actions: <Type, Action<Intent>> {
 1359                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1360               },
 1361               child: Builder(
 1362                 builder: (BuildContext context2) {
 1363                   return Actions(
 1364                     actions: <Type, Action<Intent>> {
 1365                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2),
 1366                     },
 1367                     child: Builder(
 1368                       builder: (BuildContext context3) {
 1369                         return Actions(
 1370                           actions: <Type, Action<Intent>> {
 1371                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1372                           },
 1373                           child: Builder(
 1374                             builder: (BuildContext context4) {
 1375                               invokingContext = context4;
 1376                               return const SizedBox();
 1377                             },
 1378                           ),
 1379                         );
 1380                       },
 1381                     ),
 1382                   );
 1383                 },
 1384               ),
 1385             );
 1386           },
 1387         ),
 1388       );
 1389 
 1390       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1391       expect(invocations, <String>[
 1392         'action1.invokeAsOverride-pre-super',
 1393         'action2.invokeAsOverride-pre-super',
 1394         'action3.invoke',
 1395         'action2.invokeAsOverride-post-super',
 1396         'action1.invokeAsOverride-post-super',
 1397       ]);
 1398 
 1399       invocations.clear();
 1400       await tester.pumpWidget(
 1401         Builder(
 1402           builder: (BuildContext context1) {
 1403             return Actions(
 1404               actions: <Type, Action<Intent>> {
 1405                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1', enabled: false), context: context1),
 1406               },
 1407               child: Builder(
 1408                 builder: (BuildContext context2) {
 1409                   return Actions(
 1410                     actions: <Type, Action<Intent>> {
 1411                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2),
 1412                     },
 1413                     child: Builder(
 1414                       builder: (BuildContext context3) {
 1415                         return Actions(
 1416                           actions: <Type, Action<Intent>> {
 1417                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1418                           },
 1419                           child: Builder(
 1420                             builder: (BuildContext context4) {
 1421                               invokingContext = context4;
 1422                               return const SizedBox();
 1423                             },
 1424                           ),
 1425                         );
 1426                       },
 1427                     ),
 1428                   );
 1429                 },
 1430               ),
 1431             );
 1432           },
 1433         ),
 1434       );
 1435 
 1436       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1437       expect(invocations, <String>[]);
 1438     });
 1439 
 1440     testWidgets('The override can choose to defer isActionEnabled to the overridable', (WidgetTester tester) async {
 1441       await tester.pumpWidget(
 1442         Builder(
 1443           builder: (BuildContext context1) {
 1444             return Actions(
 1445               actions: <Type, Action<Intent>> {
 1446                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationButDeferIsEnabledAction(actionName: 'action1'), context: context1),
 1447               },
 1448               child: Builder(
 1449                 builder: (BuildContext context2) {
 1450                   return Actions(
 1451                     actions: <Type, Action<Intent>> {
 1452                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2),
 1453                     },
 1454                     child: Builder(
 1455                       builder: (BuildContext context3) {
 1456                         return Actions(
 1457                           actions: <Type, Action<Intent>> {
 1458                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1459                           },
 1460                           child: Builder(
 1461                             builder: (BuildContext context4) {
 1462                               invokingContext = context4;
 1463                               return const SizedBox();
 1464                             },
 1465                           ),
 1466                         );
 1467                       },
 1468                     ),
 1469                   );
 1470                 },
 1471               ),
 1472             );
 1473           },
 1474         ),
 1475       );
 1476 
 1477       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1478       // Nothing since the final override defers its isActionEnabled state to action2,
 1479       // which is disabled.
 1480       expect(invocations, <String>[]);
 1481 
 1482       invocations.clear();
 1483       await tester.pumpWidget(
 1484         Builder(
 1485           builder: (BuildContext context1) {
 1486             return Actions(
 1487               actions: <Type, Action<Intent>> {
 1488                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1489               },
 1490               child: Builder(
 1491                 builder: (BuildContext context2) {
 1492                   return Actions(
 1493                     actions: <Type, Action<Intent>> {
 1494                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationButDeferIsEnabledAction(actionName: 'action2'), context: context2),
 1495                     },
 1496                     child: Builder(
 1497                       builder: (BuildContext context3) {
 1498                         return Actions(
 1499                           actions: <Type, Action<Intent>> {
 1500                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3', enabled: false), context: context3),
 1501                           },
 1502                           child: Builder(
 1503                             builder: (BuildContext context4) {
 1504                               invokingContext = context4;
 1505                               return const SizedBox();
 1506                             },
 1507                           ),
 1508                         );
 1509                       },
 1510                     ),
 1511                   );
 1512                 },
 1513               ),
 1514             );
 1515           },
 1516         ),
 1517       );
 1518 
 1519       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1520       // The final override (action1) is enabled so all 3 actions are enabled.
 1521       expect(invocations, <String>[
 1522         'action1.invokeAsOverride-pre-super',
 1523         'action2.invokeAsOverride-pre-super',
 1524         'action3.invoke',
 1525         'action2.invokeAsOverride-post-super',
 1526         'action1.invokeAsOverride-post-super',
 1527       ]);
 1528     });
 1529 
 1530     testWidgets('Throws on infinite recursions', (WidgetTester tester) async {
 1531       late StateSetter setState;
 1532       BuildContext? action2LookupContext;
 1533       await tester.pumpWidget(
 1534         Builder(
 1535           builder: (BuildContext context1) {
 1536             return Actions(
 1537               actions: <Type, Action<Intent>> {
 1538                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1539               },
 1540               child: StatefulBuilder(
 1541                 builder: (BuildContext context2, StateSetter stateSetter) {
 1542                   setState = stateSetter;
 1543                   return Actions(
 1544                     actions: <Type, Action<Intent>> {
 1545                       if (action2LookupContext != null) LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: action2LookupContext!),
 1546                     },
 1547                     child: Builder(
 1548                       builder: (BuildContext context3) {
 1549                         return Actions(
 1550                           actions: <Type, Action<Intent>> {
 1551                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1552                           },
 1553                           child: Builder(
 1554                             builder: (BuildContext context4) {
 1555                               invokingContext = context4;
 1556                               return const SizedBox();
 1557                             },
 1558                           ),
 1559                         );
 1560                       },
 1561                     ),
 1562                   );
 1563                 },
 1564               ),
 1565             );
 1566           },
 1567         ),
 1568       );
 1569 
 1570       // Let action2 look up its override using a context below itself, so it
 1571       // will find action3 as its override.
 1572       expect(tester.takeException(), isNull);
 1573       setState(() {
 1574         action2LookupContext = invokingContext;
 1575       });
 1576 
 1577       await tester.pump();
 1578       expect(tester.takeException(), isNull);
 1579 
 1580       Object? exception;
 1581       try {
 1582         Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1583       } catch (e) {
 1584         exception = e;
 1585       }
 1586       expect(exception?.toString(), contains('debugAssertIsEnabledMutuallyRecursive'));
 1587     });
 1588 
 1589     testWidgets('Throws on invoking invalid override', (WidgetTester tester) async {
 1590       await tester.pumpWidget(
 1591         Builder(
 1592           builder: (BuildContext context) {
 1593             return Actions(
 1594               actions: <Type, Action<Intent>> { LogIntent : TestContextAction() },
 1595               child: Builder(
 1596                 builder: (BuildContext context) {
 1597                   return Actions(
 1598                     actions: <Type, Action<Intent>> {
 1599                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context),
 1600                     },
 1601                     child: Builder(
 1602                       builder: (BuildContext context1) {
 1603                         invokingContext = context1;
 1604                         return const SizedBox();
 1605                       },
 1606                     ),
 1607                   );
 1608                 },
 1609               ),
 1610             );
 1611           },
 1612         ),
 1613       );
 1614 
 1615       Object? exception;
 1616       try {
 1617         Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1618       } catch (e) {
 1619         exception = e;
 1620       }
 1621       expect(
 1622         exception?.toString(),
 1623         contains('cannot be handled by an Action of runtime type TestContextAction.'),
 1624       );
 1625     });
 1626 
 1627     testWidgets('Make an overridable action overridable', (WidgetTester tester) async {
 1628       await tester.pumpWidget(
 1629         Builder(
 1630           builder: (BuildContext context1) {
 1631             return Actions(
 1632               actions: <Type, Action<Intent>> {
 1633                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1634               },
 1635               child: Builder(
 1636                 builder: (BuildContext context2) {
 1637                   return Actions(
 1638                     actions: <Type, Action<Intent>> {
 1639                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2),
 1640                     },
 1641                     child: Builder(
 1642                       builder: (BuildContext context3) {
 1643                         return Actions(
 1644                           actions: <Type, Action<Intent>> {
 1645                             LogIntent: Action<LogIntent>.overridable(
 1646                               defaultAction: Action<LogIntent>.overridable(
 1647                                 defaultAction: Action<LogIntent>.overridable(
 1648                                   defaultAction: LogInvocationAction(actionName: 'action3'),
 1649                                   context: context1,
 1650                                 ),
 1651                                 context: context2,
 1652                               ),
 1653                               context: context3,
 1654                             ),
 1655                           },
 1656                           child: Builder(
 1657                             builder: (BuildContext context4) {
 1658                               invokingContext = context4;
 1659                               return const SizedBox();
 1660                             },
 1661                           ),
 1662                         );
 1663                       },
 1664                     ),
 1665                   );
 1666                 },
 1667               ),
 1668             );
 1669           },
 1670         ),
 1671       );
 1672 
 1673       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1674       expect(invocations, <String>[
 1675         'action1.invokeAsOverride-pre-super',
 1676         'action2.invokeAsOverride-pre-super',
 1677         'action3.invoke',
 1678         'action2.invokeAsOverride-post-super',
 1679         'action1.invokeAsOverride-post-super',
 1680       ]);
 1681     });
 1682 
 1683     testWidgets('Overriding Actions can change the intent', (WidgetTester tester) async {
 1684       final List<String> newLogChannel = <String>[];
 1685       await tester.pumpWidget(
 1686         Builder(
 1687           builder: (BuildContext context1) {
 1688             return Actions(
 1689               actions: <Type, Action<Intent>> {
 1690                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1691               },
 1692               child: Builder(
 1693                 builder: (BuildContext context2) {
 1694                   return Actions(
 1695                     actions: <Type, Action<Intent>> {
 1696                       LogIntent: Action<LogIntent>.overridable(defaultAction: RedirectOutputAction(actionName: 'action2', newLog: newLogChannel), context: context2),
 1697                     },
 1698                     child: Builder(
 1699                       builder: (BuildContext context3) {
 1700                         return Actions(
 1701                           actions: <Type, Action<Intent>> {
 1702                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1703                           },
 1704                           child: Builder(
 1705                             builder: (BuildContext context4) {
 1706                               invokingContext = context4;
 1707                               return const SizedBox();
 1708                             },
 1709                           ),
 1710                         );
 1711                       },
 1712                     ),
 1713                   );
 1714                 },
 1715               ),
 1716             );
 1717           },
 1718         ),
 1719       );
 1720 
 1721       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1722       expect(invocations, <String>[
 1723         'action1.invokeAsOverride-pre-super',
 1724         'action1.invokeAsOverride-post-super',
 1725       ]);
 1726       expect(newLogChannel, <String>[
 1727         'action2.invokeAsOverride-pre-super',
 1728         'action3.invoke',
 1729         'action2.invokeAsOverride-post-super',
 1730       ]);
 1731     });
 1732 
 1733     testWidgets('Override non-context overridable Actions with a ContextAction', (WidgetTester tester) async {
 1734       await tester.pumpWidget(
 1735         Builder(
 1736           builder: (BuildContext context1) {
 1737             return Actions(
 1738               actions: <Type, Action<Intent>> {
 1739                 // The default Action is a ContextAction subclass.
 1740                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationContextAction(actionName: 'action1'), context: context1),
 1741               },
 1742               child: Builder(
 1743                 builder: (BuildContext context2) {
 1744                   return Actions(
 1745                     actions: <Type, Action<Intent>> {
 1746                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2),
 1747                     },
 1748                     child: Builder(
 1749                       builder: (BuildContext context3) {
 1750                         return Actions(
 1751                           actions: <Type, Action<Intent>> {
 1752                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1753                           },
 1754                           child: Builder(
 1755                             builder: (BuildContext context4) {
 1756                               invokingContext = context4;
 1757                               return const SizedBox();
 1758                             },
 1759                           ),
 1760                         );
 1761                       },
 1762                     ),
 1763                   );
 1764                 },
 1765               ),
 1766             );
 1767           },
 1768         ),
 1769       );
 1770 
 1771       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1772       expect(invocations, <String>[
 1773         'action1.invokeAsOverride-pre-super',
 1774         'action2.invokeAsOverride-pre-super',
 1775         'action3.invoke',
 1776         'action2.invokeAsOverride-post-super',
 1777         'action1.invokeAsOverride-post-super',
 1778       ]);
 1779 
 1780       // Action1 is a ContextAction and action2 & action3 are not.
 1781       // They should not lose information.
 1782       expect(LogInvocationContextAction.invokeContext, isNotNull);
 1783       expect(LogInvocationContextAction.invokeContext, invokingContext);
 1784     });
 1785 
 1786     testWidgets('Override a ContextAction with a regular Action', (WidgetTester tester) async {
 1787       await tester.pumpWidget(
 1788         Builder(
 1789           builder: (BuildContext context1) {
 1790             return Actions(
 1791               actions: <Type, Action<Intent>> {
 1792                 LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1),
 1793               },
 1794               child: Builder(
 1795                 builder: (BuildContext context2) {
 1796                   return Actions(
 1797                     actions: <Type, Action<Intent>> {
 1798                       LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationContextAction(actionName: 'action2', enabled: false), context: context2),
 1799                     },
 1800                     child: Builder(
 1801                       builder: (BuildContext context3) {
 1802                         return Actions(
 1803                           actions: <Type, Action<Intent>> {
 1804                             LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3),
 1805                           },
 1806                           child: Builder(
 1807                             builder: (BuildContext context4) {
 1808                               invokingContext = context4;
 1809                               return const SizedBox();
 1810                             },
 1811                           ),
 1812                         );
 1813                       },
 1814                     ),
 1815                   );
 1816                 },
 1817               ),
 1818             );
 1819           },
 1820         ),
 1821       );
 1822 
 1823       Actions.invoke(invokingContext!, LogIntent(log: invocations));
 1824       expect(invocations, <String>[
 1825         'action1.invokeAsOverride-pre-super',
 1826         'action2.invokeAsOverride-pre-super',
 1827         'action3.invoke',
 1828         'action2.invokeAsOverride-post-super',
 1829         'action1.invokeAsOverride-post-super',
 1830       ]);
 1831 
 1832       // Action2 is a ContextAction and action1 & action2 are regular actions.
 1833       // Invoking action2 from action3 should still supply a non-null
 1834       // BuildContext.
 1835       expect(LogInvocationContextAction.invokeContext, isNotNull);
 1836       expect(LogInvocationContextAction.invokeContext, invokingContext);
 1837     });
 1838   });
 1839 }
 1840 
 1841 typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, ActionDispatcher dispatcher});
 1842 
 1843 class TestIntent extends Intent {
 1844   const TestIntent();
 1845 }
 1846 
 1847 class SecondTestIntent extends TestIntent {
 1848   const SecondTestIntent();
 1849 }
 1850 
 1851 class ThirdTestIntent extends SecondTestIntent {
 1852   const ThirdTestIntent();
 1853 }
 1854 
 1855 class TestAction extends CallbackAction<TestIntent> {
 1856   TestAction({
 1857     required OnInvokeCallback onInvoke,
 1858   })  : assert(onInvoke != null),
 1859         super(onInvoke: onInvoke);
 1860 
 1861   @override
 1862   bool isEnabled(TestIntent intent) => enabled;
 1863 
 1864   bool get enabled => _enabled;
 1865   bool _enabled = true;
 1866   set enabled(bool value) {
 1867     if (_enabled == value) {
 1868       return;
 1869     }
 1870     _enabled = value;
 1871     notifyActionListeners();
 1872   }
 1873 
 1874   @override
 1875   void addActionListener(ActionListenerCallback listener) {
 1876     super.addActionListener(listener);
 1877     listeners.add(listener);
 1878   }
 1879 
 1880   @override
 1881   void removeActionListener(ActionListenerCallback listener) {
 1882     super.removeActionListener(listener);
 1883     listeners.remove(listener);
 1884   }
 1885   List<ActionListenerCallback> listeners = <ActionListenerCallback>[];
 1886 
 1887   void _testInvoke(TestIntent intent) => invoke(intent);
 1888 }
 1889 
 1890 class TestDispatcher extends ActionDispatcher {
 1891   const TestDispatcher({this.postInvoke});
 1892 
 1893   final PostInvokeCallback? postInvoke;
 1894 
 1895   @override
 1896   Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) {
 1897     final Object? result = super.invokeAction(action, intent, context);
 1898     postInvoke?.call(action: action, intent: intent, dispatcher: this);
 1899     return result;
 1900   }
 1901 }
 1902 
 1903 class TestDispatcher1 extends TestDispatcher {
 1904   const TestDispatcher1({super.postInvoke});
 1905 }
 1906 
 1907 class TestContextAction extends ContextAction<TestIntent> {
 1908   List<BuildContext?> capturedContexts = <BuildContext?>[];
 1909 
 1910   @override
 1911   void invoke(covariant TestIntent intent, [BuildContext? context]) {
 1912     capturedContexts.add(context);
 1913   }
 1914 }
 1915 
 1916 class LogIntent extends Intent {
 1917   const LogIntent({ required this.log });
 1918 
 1919   final List<String> log;
 1920 }
 1921 
 1922 class LogInvocationAction extends Action<LogIntent> {
 1923   LogInvocationAction({ required this.actionName, this.enabled = true });
 1924 
 1925   final String actionName;
 1926 
 1927   final bool enabled;
 1928 
 1929   @override
 1930   bool get isActionEnabled => enabled;
 1931 
 1932   @override
 1933   void invoke(LogIntent intent) {
 1934     final Action<LogIntent>? callingAction = this.callingAction;
 1935     if (callingAction == null) {
 1936       intent.log.add('$actionName.invoke');
 1937     } else {
 1938       intent.log.add('$actionName.invokeAsOverride-pre-super');
 1939       callingAction.invoke(intent);
 1940       intent.log.add('$actionName.invokeAsOverride-post-super');
 1941     }
 1942   }
 1943 
 1944   @override
 1945   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 1946     super.debugFillProperties(properties);
 1947     properties.add(StringProperty('actionName', actionName));
 1948   }
 1949 }
 1950 
 1951 class LogInvocationContextAction extends ContextAction<LogIntent> {
 1952   LogInvocationContextAction({ required this.actionName, this.enabled = true });
 1953 
 1954   static BuildContext? invokeContext;
 1955 
 1956   final String actionName;
 1957 
 1958   final bool enabled;
 1959 
 1960   @override
 1961   bool get isActionEnabled => enabled;
 1962 
 1963   @override
 1964   void invoke(LogIntent intent, [BuildContext? context]) {
 1965     invokeContext = context;
 1966     final Action<LogIntent>? callingAction = this.callingAction;
 1967     if (callingAction == null) {
 1968       intent.log.add('$actionName.invoke');
 1969     } else {
 1970       intent.log.add('$actionName.invokeAsOverride-pre-super');
 1971       callingAction.invoke(intent);
 1972       intent.log.add('$actionName.invokeAsOverride-post-super');
 1973     }
 1974   }
 1975 
 1976   @override
 1977   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 1978     super.debugFillProperties(properties);
 1979     properties.add(StringProperty('actionName', actionName));
 1980   }
 1981 }
 1982 
 1983 class LogInvocationButDeferIsEnabledAction extends LogInvocationAction {
 1984   LogInvocationButDeferIsEnabledAction({ required super.actionName });
 1985 
 1986   // Defer `isActionEnabled` to the overridable action.
 1987   @override
 1988   bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
 1989 }
 1990 
 1991 class RedirectOutputAction extends LogInvocationAction {
 1992   RedirectOutputAction({
 1993       required super.actionName,
 1994       super.enabled,
 1995       required this.newLog,
 1996   });
 1997 
 1998   final List<String> newLog;
 1999 
 2000   @override
 2001   void invoke(LogIntent intent) => super.invoke(LogIntent(log: newLog));
 2002 }