// Copyright 2019 The Chromium 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/material.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)); }); }); 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); }); }