// 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/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, BuildContext? context, ActionDispatcher dispatcher}); class TestAction extends CallbackAction<Intent> { TestAction({ required OnInvokeCallback onInvoke, }) : assert(onInvoke != null), super(onInvoke: onInvoke); static const LocalKey key = ValueKey<Type>(TestAction); } class TestDispatcher extends ActionDispatcher { const TestDispatcher({this.postInvoke}); final PostInvokeCallback? postInvoke; @override Object? invokeAction(Action<TestIntent> action, Intent intent, [BuildContext? context]) { final Object? result = super.invokeAction(action, intent, context); postInvoke?.call(action: action, intent: intent, context: context, dispatcher: this); return result; } } /// An activator that accepts down events that has [key] as the logical key. /// /// This class is used only to tests. It is intentionally designed poorly by /// returning null in [triggers], and checks [key] in [accepts]. class DumbLogicalActivator extends ShortcutActivator { const DumbLogicalActivator(this.key); final LogicalKeyboardKey key; @override Iterable<LogicalKeyboardKey>? get triggers => null; @override bool accepts(RawKeyEvent event, RawKeyboard state) { return event is RawKeyDownEvent && event.logicalKey == key; } /// Returns a short and readable description of the key combination. /// /// Intended to be used in debug mode for logging purposes. In release mode, /// [debugDescribeKeys] returns an empty string. @override String debugDescribeKeys() { String result = ''; assert(() { result = key.keyLabel; return true; }()); return result; } } class TestIntent extends Intent { const TestIntent(); } class TestIntent2 extends Intent { const TestIntent2(); } class TestShortcutManager extends ShortcutManager { TestShortcutManager(this.keys); List<LogicalKeyboardKey> keys; @override KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) { if (event is RawKeyDownEvent) { keys.add(event.logicalKey); } return super.handleKeypress(context, event); } } Widget activatorTester( ShortcutActivator activator, ValueSetter<Intent> onInvoke, [ ShortcutActivator? activator2, ValueSetter<Intent>? onInvoke2, ]) { final bool hasSecond = activator2 != null && onInvoke2 != null; return Actions( key: GlobalKey(), actions: <Type, Action<Intent>>{ TestIntent: TestAction(onInvoke: (Intent intent) { onInvoke(intent); return true; }), if (hasSecond) TestIntent2: TestAction(onInvoke: (Intent intent) { onInvoke2(intent); }), }, child: Shortcuts( shortcuts: <ShortcutActivator, Intent>{ activator: const TestIntent(), if (hasSecond) activator2: const TestIntent2(), }, child: const Focus( autofocus: true, child: SizedBox(width: 100, height: 100), ), ), ); } void main() { group(LogicalKeySet, () { test('LogicalKeySet passes parameters correctly.', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final LogicalKeySet set2 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ); final LogicalKeySet set3 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, ); final LogicalKeySet set4 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final LogicalKeySet setFromSet = LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }); expect( set1.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, }), ); expect( set2.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, }), ); expect( set3.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, }), ); expect( set4.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); expect( setFromSet.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); }); test('LogicalKeySet works as a map key.', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final LogicalKeySet set2 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final LogicalKeySet set3 = LogicalKeySet( LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, ); final LogicalKeySet set4 = LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, }); final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'}; expect(set2 == set3, isTrue); expect(set2 == set4, isTrue); expect(set2.hashCode, set3.hashCode); expect(set2.hashCode, set4.hashCode); expect(map.containsKey(set1), isTrue); expect(map.containsKey(LogicalKeySet(LogicalKeyboardKey.keyA)), isTrue); expect( set2, equals(LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, })), ); }); testWidgets('handles two keys', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( LogicalKeySet( LogicalKeyboardKey.keyC, LogicalKeyboardKey.control, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // KeyC -> LCtrl: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // LCtrl -> LShift -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); invoked = 0; // LCtrl -> KeyA -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }); test('LogicalKeySet.hashCode is stable', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); expect(set1.hashCode, set1.hashCode); final LogicalKeySet set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB); expect(set2.hashCode, set2.hashCode); final LogicalKeySet set3 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC); expect(set3.hashCode, set3.hashCode); final LogicalKeySet set4 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD); expect(set4.hashCode, set4.hashCode); }); test('LogicalKeySet.hashCode is order-independent', () { expect( LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB).hashCode, LogicalKeySet(LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC).hashCode, LogicalKeySet(LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD).hashCode, LogicalKeySet(LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); }); test('LogicalKeySet diagnostics work.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ).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(1)); expect(description[0], equals('keys: Key A + Key B')); }); }); group(SingleActivator, () { testWidgets('handles Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // KeyC -> LCtrl: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // LShift -> LCtrl -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); invoked = 0; // With Ctrl-C pressed, KeyA -> Release KeyA: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); invoked = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // LCtrl -> KeyA -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // LCtrl -> RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // While holding Ctrl-C, press KeyA: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC); expect(invoked, 2); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 2); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, shift: true, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LShift -> LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; // LCtrl -> LShift -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; // LCtrl -> KeyC -> LShift: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }); test('diagnostics.', () { { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, ).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(1)); expect(description[0], equals('keys: Key A')); } { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, control: true, shift: true, alt: true, meta: true, ).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(1)); expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A')); } }); }); group(Shortcuts, () { testWidgets('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async { final ShortcutManager manager = ShortcutManager(); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, isEmpty); const Shortcuts shortcuts = Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); testWidgets('Shortcuts.of and maybeOf find shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); await tester.pumpWidget( Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ); await tester.pump(); expect(Shortcuts.maybeOf(containerKey.currentContext!), isNotNull); expect(Shortcuts.maybeOf(containerKey.currentContext!), equals(testManager)); expect(Shortcuts.of(containerKey.currentContext!), equals(testManager)); }); testWidgets('Shortcuts.of and maybeOf work correctly without shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); await tester.pumpWidget(Container(key: containerKey)); expect(Shortcuts.maybeOf(containerKey.currentContext!), isNull); late FlutterError error; try { Shortcuts.of(containerKey.currentContext!); } on FlutterError catch (e) { error = e; } finally { expect(error, isNotNull); expect(error.diagnostics.length, 5); expect(error.diagnostics[2].level, DiagnosticLevel.info); expect( error.diagnostics[2].toStringDeep(), 'No Shortcuts ancestor could be found starting from the context\n' 'that was passed to Shortcuts.of().\n', ); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' Unable to find a Shortcuts widget in the context.\n' ' Shortcuts.of() was called with a context that does not contain a\n' ' Shortcuts widget.\n' ' No Shortcuts ancestor could be found starting from the context\n' ' that was passed to Shortcuts.of().\n' ' The context used was:\n' ' Container-[GlobalKey#00000]\n', )); } }); testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ); await tester.pump(); expect(Shortcuts.of(containerKey.currentContext!), isNotNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); testWidgets('ShortcutManager ignores keypresses with no primary focus', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ); await tester.pump(); expect(primaryFocus, isNull); expect(Shortcuts.of(containerKey.currentContext!), isNotNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isFalse); expect(pressedKeys, isEmpty); }); testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ); await tester.pump(); expect(Shortcuts.of(containerKey.currentContext!), isNotNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ), ); await tester.pump(); expect(Shortcuts.of(containerKey.currentContext!), isNotNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isFalse); expect(pressedKeys, isEmpty); }); testWidgets("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ); await tester.pump(); expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull); final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(handled, isFalse); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); }); testWidgets('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ); await tester.pump(); expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); expect(invoked, isTrue); }); testWidgets('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: DoNothingAction(consumesKey: false), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isFalse); expect(invoked, isFalse); }); testWidgets('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts( manager: testManager, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): DoNothingAndStopPropagationIntent(), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isFalse); expect(invoked, isFalse); }); test('Shortcuts diagnostics work.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.shift, LogicalKeyboardKey.keyA, ): const ActivateIntent(), LogicalKeySet( LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight, ): const DirectionalFocusIntent(TraversalDirection.right), }, child: const SizedBox(), ).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(1)); expect( description[0], equalsIgnoringHashCodes( 'shortcuts: {{Shift + Key A}: ActivateIntent#00000, {Shift + Arrow Right}: DirectionalFocusIntent#00000}', ), ); }); test('Shortcuts diagnostics work when debugLabel specified.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( debugLabel: '<Debug Label>', shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ): const ActivateIntent(), }, child: const SizedBox(), ).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(1)); expect(description[0], equals('shortcuts: <Debug Label>')); }); test('Shortcuts diagnostics work when manager specified.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( manager: ShortcutManager(), shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ): const ActivateIntent(), }, child: const SizedBox(), ).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('manager: ShortcutManager#00000(shortcuts: {})')); expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}')); }); testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp() { return MaterialApp( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents( orderedIntents: <Intent>[ ActivateIntent(), ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page), ], ), LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(), LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page), }, home: Material( child: Center( child: ListView( primary: true, children: <Widget> [ StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Checkbox( value: value, onChanged: (bool? newValue) => setState(() { value = newValue; }), focusColor: Colors.orange[500], ); }, ), Container( color: Colors.blue, height: 1000, ), ], ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( tester.binding.focusManager.primaryFocus!.toStringShort(), equalsIgnoringHashCodes('FocusScopeNode#00000(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])'), ); final ScrollController controller = PrimaryScrollController.of( tester.element(find.byType(ListView)), )!; expect(controller.position.pixels, 0.0); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); // ScrollView scrolls expect(controller.position.pixels, 448.0); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); // Focus is now on the checkbox. expect( tester.binding.focusManager.primaryFocus!.toStringShort(), equalsIgnoringHashCodes('FocusNode#00000([PRIMARY FOCUS])'), ); expect(value, isTrue); expect(controller.position.pixels, 0.0); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); // Checkbox is toggled, scroll view does not scroll. expect(value, isFalse); expect(controller.position.pixels, 0.0); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(value, isTrue); expect(controller.position.pixels, 0.0); }); testWidgets('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const DumbLogicalActivator(LogicalKeyboardKey.keyC), (Intent intent) { invoked += 1; }, const SingleActivator(LogicalKeyboardKey.keyC, control: true), (Intent intent) { invoked += 10; }, )); await tester.pump(); // Press KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); invoked = 0; // Press ControlLeft + KeyC: Accepted by SingleActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 10); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 10); invoked = 0; // Press ControlLeft + ShiftLeft + KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; }); }); group('CharacterActivator', () { testWidgets('is triggered on events with correct character', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 2); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 2); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); }); group('CallbackShortcuts', () { testWidgets('trigger on key events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invoked += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invoked, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invoked, equals(1)); }); testWidgets('nested CallbackShortcuts stop propagation', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invokedInner += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); }); testWidgets('non-overlapping nested CallbackShortcuts fire appropriately', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('b'): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('a'): () { invokedInner += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); }); testWidgets('Works correctly with Shortcuts too', (WidgetTester tester) async { int invokedCallbackA = 0; int invokedCallbackB = 0; int invokedActionA = 0; int invokedActionB = 0; void clear() { invokedCallbackA = 0; invokedCallbackB = 0; invokedActionA = 0; invokedActionB = 0; } await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invokedActionA += 1; return true; }, ), TestIntent2: TestAction( onInvoke: (Intent intent) { invokedActionB += 1; return true; }, ), }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('b'): () { invokedCallbackB += 1; }, }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), LogicalKeySet(LogicalKeyboardKey.keyB): const TestIntent2(), }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('a'): () { invokedCallbackA += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedCallbackA, equals(1)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); clear(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedCallbackA, equals(0)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); }); }); }