blob: 932b7fe1c9de19358b2fd28658f3ae31a559239d [file] [log] [blame]
// 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);
});
}