"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 }