// 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/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; typedef PostInvokeCallback = void Function({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher}); class TestAction extends CallbackAction { const TestAction({ @required OnInvokeCallback onInvoke, }) : assert(onInvoke != null), super(key, onInvoke: onInvoke); static const LocalKey key = ValueKey<Type>(TestAction); void _testInvoke(FocusNode node, Intent invocation) => invoke(node, invocation); } class TestDispatcher extends ActionDispatcher { const TestDispatcher({this.postInvoke}); final PostInvokeCallback postInvoke; @override bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) { final bool result = super.invokeAction(action, intent, focusNode: focusNode); postInvoke?.call(action: action, intent: intent, focusNode: focusNode, dispatcher: this); return result; } } class TestDispatcher1 extends TestDispatcher { const TestDispatcher1({PostInvokeCallback postInvoke}) : super(postInvoke: postInvoke); } void main() { test('Action passes parameters on when invoked.', () { bool invoked = false; FocusNode passedNode; final TestAction action = TestAction(onInvoke: (FocusNode node, Intent invocation) { invoked = true; passedNode = node; }); final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); action._testInvoke(testNode, null); expect(passedNode, equals(testNode)); expect(action.intentKey, equals(TestAction.key)); expect(invoked, isTrue); }); group(ActionDispatcher, () { test('ActionDispatcher invokes actions when asked.', () { bool invoked = false; FocusNode passedNode; const ActionDispatcher dispatcher = ActionDispatcher(); final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); final bool result = dispatcher.invokeAction( TestAction( onInvoke: (FocusNode node, Intent invocation) { invoked = true; passedNode = node; }, ), const Intent(TestAction.key), focusNode: testNode, ); expect(passedNode, equals(testNode)); expect(result, isTrue); expect(invoked, isTrue); }); }); group(Actions, () { Intent invokedIntent; Action invokedAction; FocusNode invokedNode; ActionDispatcher invokedDispatcher; void collect({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher}) { invokedIntent = intent; invokedAction = action; invokedNode = focusNode; invokedDispatcher = dispatcher; } void clear() { invokedIntent = null; invokedAction = null; invokedNode = null; invokedDispatcher = null; } setUp(clear); testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; FocusNode passedNode; final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); await tester.pumpWidget( Actions( actions: <LocalKey, ActionFactory>{ TestAction.key: () => TestAction( onInvoke: (FocusNode node, Intent invocation) { invoked = true; passedNode = node; }, ), }, child: Container(key: containerKey), ), ); await tester.pump(); final bool result = Actions.invoke( containerKey.currentContext, const Intent(TestAction.key), focusNode: testNode, ); expect(passedNode, equals(testNode)); expect(result, isTrue); expect(invoked, isTrue); }); testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); bool invoked = false; const Intent intent = Intent(TestAction.key); FocusNode passedNode; final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); final Action testAction = TestAction( onInvoke: (FocusNode node, Intent intent) { invoked = true; passedNode = node; }, ); await tester.pumpWidget( Actions( dispatcher: TestDispatcher(postInvoke: collect), actions: <LocalKey, ActionFactory>{ TestAction.key: () => testAction, }, child: Container(key: containerKey), ), ); await tester.pump(); final bool result = Actions.invoke( containerKey.currentContext, intent, focusNode: testNode, ); expect(passedNode, equals(testNode)); expect(invokedNode, equals(testNode)); 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 Intent intent = Intent(TestAction.key); FocusNode passedNode; final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); final Action testAction = TestAction( onInvoke: (FocusNode node, Intent invocation) { invoked = true; passedNode = node; }, ); await tester.pumpWidget( Actions( dispatcher: TestDispatcher1(postInvoke: collect), actions: <LocalKey, ActionFactory>{ TestAction.key: () => testAction, }, child: Actions( dispatcher: TestDispatcher(postInvoke: collect), actions: const <LocalKey, ActionFactory>{}, child: Container(key: containerKey), ), ), ); await tester.pump(); final bool result = Actions.invoke( containerKey.currentContext, intent, focusNode: testNode, ); expect(passedNode, equals(testNode)); expect(invokedNode, equals(testNode)); 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 Intent intent = Intent(TestAction.key); FocusNode passedNode; final FocusNode testNode = FocusNode(debugLabel: 'Test Node'); final Action testAction = TestAction( onInvoke: (FocusNode node, Intent invocation) { invoked = true; passedNode = node; }, ); await tester.pumpWidget( Actions( dispatcher: TestDispatcher1(postInvoke: collect), actions: <LocalKey, ActionFactory>{ TestAction.key: () => testAction, }, child: Actions( actions: const <LocalKey, ActionFactory>{}, child: Container(key: containerKey), ), ), ); await tester.pump(); final bool result = Actions.invoke( containerKey.currentContext, intent, focusNode: testNode, ); expect(passedNode, equals(testNode)); expect(invokedNode, equals(testNode)); 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 <LocalKey, ActionFactory>{}, child: Container(key: containerKey), ), ); await tester.pump(); final ActionDispatcher dispatcher = Actions.of( containerKey.currentContext, nullOk: true, ); expect(dispatcher, equals(testDispatcher)); }); 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 = Intent(TestAction.key); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final Action testAction = TestAction( onInvoke: (FocusNode node, Intent invocation) { invoked = true; }, ); bool hovering = false; bool focusing = false; Future<void> buildTest(bool enabled) async { await tester.pumpWidget( Center( child: Actions( dispatcher: TestDispatcher1(postInvoke: collect), actions: const <LocalKey, ActionFactory>{}, child: FocusableActionDetector( enabled: enabled, focusNode: focusNode, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.enter): intent, }, actions: <LocalKey, ActionFactory>{ TestAction.key: () => testAction, }, onShowHoverHighlight: (bool value) => hovering = value, onShowFocusHighlight: (bool value) => focusing = value, child: Container(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); }); }); group('Diagnostics', () { testWidgets('default Intent debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const Intent(ValueKey<String>('foo')).debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, equals(<String>["key: [<'foo'>]"])); }); testWidgets('CallbackAction debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); CallbackAction( const ValueKey<String>('foo'), onInvoke: (FocusNode node, Intent intent) {}, ).debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, equals(<String>["intentKey: [<'foo'>]"])); }); testWidgets('default Actions debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Actions( actions: const <LocalKey, ActionFactory>{}, 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[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: <LocalKey, ActionFactory>{ const ValueKey<String>('bar'): () => TestAction(onInvoke: (FocusNode node, Intent intent) {}), }, 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[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); expect(description[1], equals("actions: {[<'bar'>]: Closure: () => TestAction}")); }, skip: isBrowser); }); }