| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, ActionDispatcher dispatcher}); |
| |
| class TestIntent extends Intent { |
| const TestIntent(); |
| } |
| |
| class SecondTestIntent extends TestIntent { |
| const SecondTestIntent(); |
| } |
| |
| class ThirdTestIntent extends SecondTestIntent { |
| const ThirdTestIntent(); |
| } |
| |
| class TestAction extends CallbackAction<TestIntent> { |
| TestAction({ |
| required OnInvokeCallback onInvoke, |
| }) : assert(onInvoke != null), |
| super(onInvoke: onInvoke); |
| |
| @override |
| bool isEnabled(TestIntent intent) => enabled; |
| |
| bool get enabled => _enabled; |
| bool _enabled = true; |
| set enabled(bool value) { |
| if (_enabled == value) { |
| return; |
| } |
| _enabled = value; |
| notifyActionListeners(); |
| } |
| |
| @override |
| void addActionListener(ActionListenerCallback listener) { |
| super.addActionListener(listener); |
| listeners.add(listener); |
| } |
| |
| @override |
| void removeActionListener(ActionListenerCallback listener) { |
| super.removeActionListener(listener); |
| listeners.remove(listener); |
| } |
| List<ActionListenerCallback> listeners = <ActionListenerCallback>[]; |
| |
| void _testInvoke(TestIntent intent) => invoke(intent); |
| } |
| |
| class TestDispatcher extends ActionDispatcher { |
| const TestDispatcher({this.postInvoke}); |
| |
| final PostInvokeCallback? postInvoke; |
| |
| @override |
| Object? invokeAction(Action<Intent> action, Intent intent, [BuildContext? context]) { |
| final Object? result = super.invokeAction(action, intent, context); |
| postInvoke?.call(action: action, intent: intent, dispatcher: this); |
| return result; |
| } |
| } |
| |
| class TestDispatcher1 extends TestDispatcher { |
| const TestDispatcher1({PostInvokeCallback? postInvoke}) : super(postInvoke: postInvoke); |
| } |
| |
| void main() { |
| testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async { |
| late Intent passedIntent; |
| final TestAction action = TestAction(onInvoke: (Intent intent) { |
| passedIntent = intent; |
| return true; |
| }); |
| const TestIntent intent = TestIntent(); |
| action._testInvoke(intent); |
| expect(passedIntent, equals(intent)); |
| }); |
| group(ActionDispatcher, () { |
| testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async { |
| await tester.pumpWidget(Container()); |
| bool invoked = false; |
| const ActionDispatcher dispatcher = ActionDispatcher(); |
| final Object? result = dispatcher.invokeAction( |
| TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| const TestIntent(), |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| }); |
| }); |
| group(Actions, () { |
| Intent? invokedIntent; |
| Action<Intent>? invokedAction; |
| ActionDispatcher? invokedDispatcher; |
| |
| void collect({Action<Intent>? action, Intent? intent, ActionDispatcher? dispatcher}) { |
| invokedIntent = intent; |
| invokedAction = action; |
| invokedDispatcher = dispatcher; |
| } |
| |
| void clear() { |
| invokedIntent = null; |
| invokedAction = null; |
| invokedDispatcher = null; |
| } |
| |
| setUp(clear); |
| |
| testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke( |
| containerKey.currentContext!, |
| const TestIntent(), |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| }); |
| testWidgets('Actions widget can invoke actions with default dispatcher and maybeInvoke', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.maybeInvoke( |
| containerKey.currentContext!, |
| const TestIntent(), |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| }); |
| testWidgets('maybeInvoke returns null when no action is found', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.maybeInvoke( |
| containerKey.currentContext!, |
| DoNothingIntent(), |
| ); |
| expect(result, isNull); |
| expect(invoked, isFalse); |
| }); |
| testWidgets('invoke throws when no action is found', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.maybeInvoke( |
| containerKey.currentContext!, |
| DoNothingIntent(), |
| ); |
| expect(result, isNull); |
| expect(invoked, isFalse); |
| }); |
| testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| const TestIntent intent = TestIntent(); |
| final Action<Intent> testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| expect(invokedIntent, equals(intent)); |
| }); |
| testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| const TestIntent intent = TestIntent(); |
| final Action<Intent> testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher1(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Actions( |
| dispatcher: TestDispatcher(postInvoke: collect), |
| actions: const <Type, Action<Intent>>{}, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| expect(invokedIntent, equals(intent)); |
| expect(invokedAction, equals(testAction)); |
| expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); |
| }); |
| testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| const TestIntent intent = TestIntent(); |
| final Action<Intent> testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher1(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Actions( |
| actions: const <Type, Action<Intent>>{}, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(result, isTrue); |
| expect(invoked, isTrue); |
| expect(invokedIntent, equals(intent)); |
| expect(invokedAction, equals(testAction)); |
| expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); |
| }); |
| testWidgets('Actions widget can be found with of', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: testDispatcher, |
| actions: const <Type, Action<Intent>>{}, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final ActionDispatcher dispatcher = Actions.of(containerKey.currentContext!); |
| expect(dispatcher, equals(testDispatcher)); |
| }); |
| testWidgets('Action can be found with find', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect); |
| bool invoked = false; |
| final TestAction testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: testDispatcher, |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Actions( |
| actions: const <Type, Action<Intent>>{}, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(Actions.find<TestIntent>(containerKey.currentContext!), equals(testAction)); |
| expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext!), throwsAssertionError); |
| expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: testDispatcher, |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Actions( |
| actions: const <Type, Action<Intent>>{}, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| expect(Actions.find<TestIntent>(containerKey.currentContext!), equals(testAction)); |
| expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext!), throwsAssertionError); |
| expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull); |
| }); |
| testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { |
| FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| const Intent intent = TestIntent(); |
| final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); |
| final Action<Intent> testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| bool hovering = false; |
| bool focusing = false; |
| |
| Future<void> buildTest(bool enabled) async { |
| await tester.pumpWidget( |
| Center( |
| child: Actions( |
| dispatcher: TestDispatcher1(postInvoke: collect), |
| actions: const <Type, Action<Intent>>{}, |
| child: FocusableActionDetector( |
| enabled: enabled, |
| focusNode: focusNode, |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.enter): intent, |
| }, |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| onShowHoverHighlight: (bool value) => hovering = value, |
| onShowFocusHighlight: (bool value) => focusing = value, |
| child: SizedBox(width: 100, height: 100, key: containerKey), |
| ), |
| ), |
| ), |
| ); |
| return tester.pump(); |
| } |
| |
| await buildTest(true); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(tester.getCenter(find.byKey(containerKey))); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| expect(hovering, isTrue); |
| expect(focusing, isTrue); |
| expect(invoked, isTrue); |
| |
| invoked = false; |
| await buildTest(false); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| await tester.pump(); |
| expect(invoked, isFalse); |
| await buildTest(true); |
| expect(focusing, isFalse); |
| expect(hovering, isTrue); |
| await buildTest(false); |
| expect(focusing, isFalse); |
| expect(hovering, isFalse); |
| await gesture.moveTo(Offset.zero); |
| await buildTest(true); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| }); |
| testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: FocusableActionDetector( |
| mouseCursor: SystemMouseCursors.text, |
| onShowHoverHighlight: (_) {}, |
| onShowFocusHighlight: (_) {}, |
| child: Container(), |
| ), |
| ), |
| ); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); |
| await gesture.addPointer(location: const Offset(1, 1)); |
| addTearDown(gesture.removePointer); |
| await tester.pump(); |
| |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); |
| |
| // Test default |
| await tester.pumpWidget( |
| MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: FocusableActionDetector( |
| onShowHoverHighlight: (_) {}, |
| onShowFocusHighlight: (_) {}, |
| child: Container(), |
| ), |
| ), |
| ); |
| |
| expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); |
| }); |
| testWidgets('Actions.invoke returns the value of Action.invoke', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| final Object sentinel = Object(); |
| bool invoked = false; |
| const TestIntent intent = TestIntent(); |
| final Action<Intent> testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return sentinel; |
| }, |
| ); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(identical(result, sentinel), isTrue); |
| expect(invoked, isTrue); |
| }); |
| testWidgets('ContextAction can return null', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| const TestIntent intent = TestIntent(); |
| final TestContextAction testAction = TestContextAction(); |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher1(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(result, isNull); |
| expect(invokedIntent, equals(intent)); |
| expect(invokedAction, equals(testAction)); |
| expect(invokedDispatcher.runtimeType, equals(TestDispatcher1)); |
| expect(testAction.capturedContexts.single, containerKey.currentContext); |
| }); |
| testWidgets('Disabled actions stop propagation to an ancestor', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked = false; |
| const TestIntent intent = TestIntent(); |
| final TestAction enabledTestAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| enabledTestAction.enabled = true; |
| final TestAction disabledTestAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| disabledTestAction.enabled = false; |
| |
| await tester.pumpWidget( |
| Actions( |
| dispatcher: TestDispatcher1(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: enabledTestAction, |
| }, |
| child: Actions( |
| dispatcher: TestDispatcher(postInvoke: collect), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: disabledTestAction, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| final Object? result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| intent, |
| ); |
| expect(result, isNull); |
| expect(invoked, isFalse); |
| expect(invokedIntent, isNull); |
| expect(invokedAction, isNull); |
| expect(invokedDispatcher, isNull); |
| }); |
| }); |
| |
| group('Listening', () { |
| testWidgets('can listen to enabled state of Actions', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| bool invoked1 = false; |
| bool invoked2 = false; |
| bool invoked3 = false; |
| final TestAction action1 = TestAction( |
| onInvoke: (Intent intent) { |
| invoked1 = true; |
| return invoked1; |
| }, |
| ); |
| final TestAction action2 = TestAction( |
| onInvoke: (Intent intent) { |
| invoked2 = true; |
| return invoked2; |
| }, |
| ); |
| final TestAction action3 = TestAction( |
| onInvoke: (Intent intent) { |
| invoked3 = true; |
| return invoked3; |
| }, |
| ); |
| bool enabled1 = true; |
| action1.addActionListener((Action<Intent> action) => enabled1 = action.isEnabled(const TestIntent())); |
| action1.enabled = false; |
| expect(enabled1, isFalse); |
| |
| bool enabled2 = true; |
| action2.addActionListener((Action<Intent> action) => enabled2 = action.isEnabled(const SecondTestIntent())); |
| action2.enabled = false; |
| expect(enabled2, isFalse); |
| |
| bool enabled3 = true; |
| action3.addActionListener((Action<Intent> action) => enabled3 = action.isEnabled(const ThirdTestIntent())); |
| action3.enabled = false; |
| expect(enabled3, isFalse); |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<TestIntent>>{ |
| TestIntent: action1, |
| SecondTestIntent: action2, |
| }, |
| child: Actions( |
| actions: <Type, Action<TestIntent>>{ |
| ThirdTestIntent: action3, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| Object? result = Actions.maybeInvoke( |
| containerKey.currentContext!, |
| const TestIntent(), |
| ); |
| expect(enabled1, isFalse); |
| expect(result, isNull); |
| expect(invoked1, isFalse); |
| |
| action1.enabled = true; |
| result = Actions.invoke( |
| containerKey.currentContext!, |
| const TestIntent(), |
| ); |
| expect(enabled1, isTrue); |
| expect(result, isTrue); |
| expect(invoked1, isTrue); |
| |
| bool? enabledChanged; |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: action1, |
| SecondTestIntent: action2, |
| }, |
| child: ActionListener( |
| listener: (Action<Intent> action) => enabledChanged = action.isEnabled(const ThirdTestIntent()), |
| action: action2, |
| child: Actions( |
| actions: <Type, Action<Intent>>{ |
| ThirdTestIntent: action3, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| result = Actions.maybeInvoke<TestIntent>( |
| containerKey.currentContext!, |
| const SecondTestIntent(), |
| ); |
| expect(enabledChanged, isNull); |
| expect(enabled2, isFalse); |
| expect(result, isNull); |
| expect(invoked2, isFalse); |
| |
| action2.enabled = true; |
| expect(enabledChanged, isTrue); |
| result = Actions.invoke<TestIntent>( |
| containerKey.currentContext!, |
| const SecondTestIntent(), |
| ); |
| expect(enabled2, isTrue); |
| expect(result, isTrue); |
| expect(invoked2, isTrue); |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: action1, |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{ |
| ThirdTestIntent: action3, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ), |
| ); |
| |
| expect(action1.listeners.length, equals(2)); |
| expect(action2.listeners.length, equals(1)); |
| expect(action3.listeners.length, equals(2)); |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: action1, |
| ThirdTestIntent: action3, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| expect(action1.listeners.length, equals(2)); |
| expect(action2.listeners.length, equals(1)); |
| expect(action3.listeners.length, equals(2)); |
| |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: action1, |
| }, |
| child: Container(key: containerKey), |
| ), |
| ); |
| |
| expect(action1.listeners.length, equals(2)); |
| expect(action2.listeners.length, equals(1)); |
| expect(action3.listeners.length, equals(1)); |
| |
| await tester.pumpWidget(Container()); |
| await tester.pump(); |
| |
| expect(action1.listeners.length, equals(1)); |
| expect(action2.listeners.length, equals(1)); |
| expect(action3.listeners.length, equals(1)); |
| }); |
| }); |
| |
| group(FocusableActionDetector, () { |
| const Intent intent = TestIntent(); |
| late bool invoked; |
| late bool hovering; |
| late bool focusing; |
| late FocusNode focusNode; |
| late Action<Intent> testAction; |
| |
| Future<void> pumpTest( |
| WidgetTester tester, { |
| bool enabled = true, |
| bool directional = false, |
| bool supplyCallbacks = true, |
| required Key key, |
| }) async { |
| await tester.pumpWidget( |
| MediaQuery( |
| data: MediaQueryData( |
| navigationMode: directional ? NavigationMode.directional : NavigationMode.traditional, |
| ), |
| child: Center( |
| child: Actions( |
| dispatcher: const TestDispatcher1(), |
| actions: const <Type, Action<Intent>>{}, |
| child: FocusableActionDetector( |
| enabled: enabled, |
| focusNode: focusNode, |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.enter): intent, |
| }, |
| actions: <Type, Action<Intent>>{ |
| TestIntent: testAction, |
| }, |
| onShowHoverHighlight: supplyCallbacks ? (bool value) => hovering = value : null, |
| onShowFocusHighlight: supplyCallbacks ? (bool value) => focusing = value : null, |
| child: SizedBox(width: 100, height: 100, key: key), |
| ), |
| ), |
| ), |
| ), |
| ); |
| return tester.pump(); |
| } |
| |
| setUp(() async { |
| invoked = false; |
| hovering = false; |
| focusing = false; |
| |
| focusNode = FocusNode(debugLabel: 'Test Node'); |
| testAction = TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ); |
| }); |
| |
| testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async { |
| FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final GlobalKey containerKey = GlobalKey(); |
| |
| await pumpTest(tester, key: containerKey); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(tester.getCenter(find.byKey(containerKey))); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| expect(hovering, isTrue); |
| expect(focusing, isTrue); |
| expect(invoked, isTrue); |
| |
| invoked = false; |
| await pumpTest(tester, enabled: false, key: containerKey); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| await tester.pump(); |
| expect(invoked, isFalse); |
| await pumpTest(tester, key: containerKey); |
| expect(focusing, isFalse); |
| expect(hovering, isTrue); |
| await pumpTest(tester, enabled: false, key: containerKey); |
| expect(focusing, isFalse); |
| expect(hovering, isFalse); |
| await gesture.moveTo(Offset.zero); |
| await pumpTest(tester, key: containerKey); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| }); |
| testWidgets('FocusableActionDetector shows focus highlight appropriately when focused and disabled', (WidgetTester tester) async { |
| FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final GlobalKey containerKey = GlobalKey(); |
| |
| await pumpTest(tester, key: containerKey); |
| await tester.pump(); |
| expect(focusing, isFalse); |
| |
| await pumpTest(tester, key: containerKey); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(focusing, isTrue); |
| |
| focusing = false; |
| await pumpTest(tester, enabled: false, key: containerKey); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(focusing, isFalse); |
| |
| await pumpTest(tester, enabled: false, key: containerKey); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(focusing, isFalse); |
| |
| // In directional navigation, focus should show, even if disabled. |
| await pumpTest(tester, enabled: false, key: containerKey, directional: true); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| expect(focusing, isTrue); |
| }); |
| testWidgets('FocusableActionDetector can be used without callbacks', (WidgetTester tester) async { |
| FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final GlobalKey containerKey = GlobalKey(); |
| |
| await pumpTest(tester, key: containerKey, supplyCallbacks: false); |
| focusNode.requestFocus(); |
| await tester.pump(); |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| addTearDown(gesture.removePointer); |
| await gesture.moveTo(tester.getCenter(find.byKey(containerKey))); |
| await tester.pump(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| expect(invoked, isTrue); |
| |
| invoked = false; |
| await pumpTest(tester, enabled: false, key: containerKey, supplyCallbacks: false); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
| await tester.pump(); |
| expect(invoked, isFalse); |
| await pumpTest(tester, key: containerKey, supplyCallbacks: false); |
| expect(focusing, isFalse); |
| expect(hovering, isFalse); |
| await pumpTest(tester, enabled: false, key: containerKey, supplyCallbacks: false); |
| expect(focusing, isFalse); |
| expect(hovering, isFalse); |
| await gesture.moveTo(Offset.zero); |
| await pumpTest(tester, key: containerKey, supplyCallbacks: false); |
| expect(hovering, isFalse); |
| expect(focusing, isFalse); |
| }); |
| |
| testWidgets( |
| 'FocusableActionDetector can prevent its descendants from being focusable', |
| (WidgetTester tester) async { |
| final FocusNode buttonNode = FocusNode(debugLabel: 'Test'); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: FocusableActionDetector( |
| child: MaterialButton( |
| focusNode: buttonNode, |
| child: const Text('Test'), |
| onPressed: () {}, |
| ), |
| ), |
| ), |
| ); |
| |
| // Button is focusable |
| expect(buttonNode.hasFocus, isFalse); |
| buttonNode.requestFocus(); |
| await tester.pump(); |
| expect(buttonNode.hasFocus, isTrue); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: FocusableActionDetector( |
| descendantsAreFocusable: false, |
| child: MaterialButton( |
| focusNode: buttonNode, |
| child: const Text('Test'), |
| onPressed: () {}, |
| ), |
| ), |
| ), |
| ); |
| |
| // Button is NOT focusable |
| expect(buttonNode.hasFocus, isFalse); |
| buttonNode.requestFocus(); |
| await tester.pump(); |
| expect(buttonNode.hasFocus, isFalse); |
| }, |
| ); |
| }); |
| |
| group('Diagnostics', () { |
| testWidgets('default Intent debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| // ignore: invalid_use_of_protected_member |
| const TestIntent().debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) { |
| return !node.isFiltered(DiagnosticLevel.info); |
| }) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect(description, isEmpty); |
| }); |
| testWidgets('default Actions debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| Actions( |
| actions: const <Type, Action<Intent>>{}, |
| dispatcher: const ActionDispatcher(), |
| child: Container(), |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) { |
| return !node.isFiltered(DiagnosticLevel.info); |
| }) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect(description.length, equals(2)); |
| expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); |
| expect(description[1], equals('actions: {}')); |
| }); |
| testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| Actions( |
| key: const ValueKey<String>('foo'), |
| dispatcher: const ActionDispatcher(), |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction(onInvoke: (Intent intent) => null), |
| }, |
| child: Container(key: const ValueKey<String>('baz')), |
| ).debugFillProperties(builder); |
| |
| final List<String> description = builder.properties |
| .where((DiagnosticsNode node) { |
| return !node.isFiltered(DiagnosticLevel.info); |
| }) |
| .map((DiagnosticsNode node) => node.toString()) |
| .toList(); |
| |
| expect(description.length, equals(2)); |
| expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); |
| expect(description[1], equalsIgnoringHashCodes('actions: {TestIntent: TestAction#00000}')); |
| }); |
| }); |
| |
| group('Action overriding', () { |
| final List<String> invocations = <String>[]; |
| BuildContext? invokingContext; |
| |
| tearDown(() { |
| invocations.clear(); |
| invokingContext = null; |
| }); |
| |
| testWidgets('Basic usage', (WidgetTester tester) async { |
| late BuildContext invokingContext2; |
| late BuildContext invokingContext3; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent : Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| invokingContext2 = context2; |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent : Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| invokingContext3 = context3; |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| |
| invocations.clear(); |
| // Invoke from a different (higher) context. |
| Actions.invoke(invokingContext3, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invoke', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| |
| invocations.clear(); |
| // Invoke from a different (higher) context. |
| Actions.invoke(invokingContext2, LogIntent(log: invocations)); |
| expect(invocations, <String>['action1.invoke']); |
| }); |
| |
| testWidgets('Does not break after use', (WidgetTester tester) async { |
| late BuildContext invokingContext2; |
| late BuildContext invokingContext3; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| invokingContext2 = context2; |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| invokingContext3 = context3; |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| // Invoke a bunch of times and verify it still produces the same result. |
| final List<BuildContext> randomContexts = <BuildContext>[ |
| invokingContext!, |
| invokingContext2, |
| invokingContext!, |
| invokingContext3, |
| invokingContext3, |
| invokingContext3, |
| invokingContext2, |
| ]; |
| |
| for (final BuildContext randomContext in randomContexts) { |
| Actions.invoke(randomContext, LogIntent(log: invocations)); |
| } |
| |
| invocations.clear(); |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| }); |
| |
| testWidgets('Does not override if not overridable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { LogIntent : LogInvocationAction(actionName: 'action2') }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| ]); |
| }); |
| |
| testWidgets('The final override controls isEnabled', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| |
| invocations.clear(); |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1', enabled: false), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[]); |
| }); |
| |
| testWidgets('The override can choose to defer isActionEnabled to the overridable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationButDeferIsEnabledAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| // Nothing since the final override defers its isActionEnabled state to action2, |
| // which is disabled. |
| expect(invocations, <String>[]); |
| |
| invocations.clear(); |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationButDeferIsEnabledAction(actionName: 'action2'), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3', enabled: false), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| // The final override (action1) is enabled so all 3 actions are enabled. |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| }); |
| |
| testWidgets('Throws on infinite recursions', (WidgetTester tester) async { |
| late StateSetter setState; |
| BuildContext? action2LookupContext; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: StatefulBuilder( |
| builder: (BuildContext context2, StateSetter stateSetter) { |
| setState = stateSetter; |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| if (action2LookupContext != null) LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: action2LookupContext!) |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| // Let action2 look up its override using a context below itself, so it |
| // will find action3 as its override. |
| expect(tester.takeException(), isNull); |
| setState(() { |
| action2LookupContext = invokingContext; |
| }); |
| |
| await tester.pump(); |
| expect(tester.takeException(), isNull); |
| |
| Object? exception; |
| try { |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| } catch (e) { |
| exception = e; |
| } |
| expect(exception?.toString(), contains('debugAssertIsEnabledMutuallyRecursive')); |
| }); |
| |
| testWidgets('Throws on invoking invalid override', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context) { |
| return Actions( |
| actions: <Type, Action<Intent>> { LogIntent : TestContextAction() }, |
| child: Builder( |
| builder: (BuildContext context) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context), |
| }, |
| child: Builder( |
| builder: (BuildContext context1) { |
| invokingContext = context1; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Object? exception; |
| try { |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| } catch (e) { |
| exception = e; |
| } |
| expect( |
| exception?.toString(), |
| contains('cannot be handled by an Action of runtime type TestContextAction.'), |
| ); |
| }); |
| |
| testWidgets('Make an overridable action overridable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2'), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable( |
| defaultAction: Action<LogIntent>.overridable( |
| defaultAction: Action<LogIntent>.overridable( |
| defaultAction: LogInvocationAction(actionName: 'action3'), |
| context: context1, |
| ), |
| context: context2, |
| ), |
| context: context3, |
| ) |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| }); |
| |
| testWidgets('Overriding Actions can change the intent', (WidgetTester tester) async { |
| final List<String> newLogChannel = <String>[]; |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: RedirectOutputAction(actionName: 'action2', newLog: newLogChannel), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| expect(newLogChannel, <String>[ |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| ]); |
| }); |
| |
| testWidgets('Override non-context overridable Actions with a ContextAction', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| // The default Action is a ContextAction subclass. |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationContextAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action2', enabled: false), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| |
| // Action1 is a ContextAction and action2 & action3 are not. |
| // They should not lose information. |
| expect(LogInvocationContextAction.invokeContext, isNotNull); |
| expect(LogInvocationContextAction.invokeContext, invokingContext); |
| }); |
| |
| testWidgets('Override a ContextAction with a regular Action', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Builder( |
| builder: (BuildContext context1) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action1'), context: context1), |
| }, |
| child: Builder( |
| builder: (BuildContext context2) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationContextAction(actionName: 'action2', enabled: false), context: context2), |
| }, |
| child: Builder( |
| builder: (BuildContext context3) { |
| return Actions( |
| actions: <Type, Action<Intent>> { |
| LogIntent: Action<LogIntent>.overridable(defaultAction: LogInvocationAction(actionName: 'action3'), context: context3), |
| }, |
| child: Builder( |
| builder: (BuildContext context4) { |
| invokingContext = context4; |
| return const SizedBox(); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| Actions.invoke(invokingContext!, LogIntent(log: invocations)); |
| expect(invocations, <String>[ |
| 'action1.invokeAsOverride-pre-super', |
| 'action2.invokeAsOverride-pre-super', |
| 'action3.invoke', |
| 'action2.invokeAsOverride-post-super', |
| 'action1.invokeAsOverride-post-super', |
| ]); |
| |
| // Action2 is a ContextAction and action1 & action2 are regular actions. |
| // Invoking action2 from action3 should still supply a non-null |
| // BuildContext. |
| expect(LogInvocationContextAction.invokeContext, isNotNull); |
| expect(LogInvocationContextAction.invokeContext, invokingContext); |
| }); |
| }); |
| } |
| |
| class TestContextAction extends ContextAction<TestIntent> { |
| List<BuildContext?> capturedContexts = <BuildContext?>[]; |
| |
| @override |
| Object? invoke(covariant TestIntent intent, [BuildContext? context]) { |
| capturedContexts.add(context); |
| return null; |
| } |
| } |
| |
| class LogIntent extends Intent { |
| const LogIntent({ required this.log }); |
| |
| final List<String> log; |
| } |
| |
| class LogInvocationAction extends Action<LogIntent> { |
| LogInvocationAction({ required this.actionName, this.enabled = true }); |
| |
| final String actionName; |
| |
| final bool enabled; |
| |
| @override |
| bool get isActionEnabled => enabled; |
| |
| @override |
| Object? invoke(LogIntent intent) { |
| final Action<LogIntent>? callingAction = this.callingAction; |
| if (callingAction == null) { |
| intent.log.add('$actionName.invoke'); |
| } else { |
| intent.log.add('$actionName.invokeAsOverride-pre-super'); |
| callingAction.invoke(intent); |
| intent.log.add('$actionName.invokeAsOverride-post-super'); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(StringProperty('actionName', actionName)); |
| } |
| } |
| |
| class LogInvocationContextAction extends ContextAction<LogIntent> { |
| LogInvocationContextAction({ required this.actionName, this.enabled = true }); |
| |
| static BuildContext? invokeContext; |
| |
| final String actionName; |
| |
| final bool enabled; |
| |
| @override |
| bool get isActionEnabled => enabled; |
| |
| @override |
| Object? invoke(LogIntent intent, [BuildContext? context]) { |
| invokeContext = context; |
| final Action<LogIntent>? callingAction = this.callingAction; |
| if (callingAction == null) { |
| intent.log.add('$actionName.invoke'); |
| } else { |
| intent.log.add('$actionName.invokeAsOverride-pre-super'); |
| callingAction.invoke(intent); |
| intent.log.add('$actionName.invokeAsOverride-post-super'); |
| } |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(StringProperty('actionName', actionName)); |
| } |
| } |
| |
| class LogInvocationButDeferIsEnabledAction extends LogInvocationAction { |
| LogInvocationButDeferIsEnabledAction({ required String actionName }) : super(actionName: actionName); |
| |
| // Defer `isActionEnabled` to the overridable action. |
| @override |
| bool get isActionEnabled => callingAction?.isActionEnabled ?? false; |
| } |
| |
| class RedirectOutputAction extends LogInvocationAction { |
| RedirectOutputAction({ |
| required String actionName, |
| bool enabled = true, |
| required this.newLog, |
| }) : super(actionName: actionName, enabled: enabled); |
| |
| final List<String> newLog; |
| |
| @override |
| Object? invoke(LogIntent intent) => super.invoke(LogIntent(log: newLog)); |
| } |