// 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'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { group(LogicalKeySet, () { test('LogicalKeySet passes parameters correctly.', () { final set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB); final set3 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, ); final set4 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final setFromSet = LogicalKeySet.fromSet({ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }); expect(set1.keys, equals({LogicalKeyboardKey.keyA})); expect( set2.keys, equals({LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB}), ); expect( set3.keys, equals({ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, }), ); expect( set4.keys, equals({ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); expect( setFromSet.keys, equals({ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); }); test('LogicalKeySet works as a map key.', () { final set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final set2 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final set3 = LogicalKeySet( LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, ); final set4 = LogicalKeySet.fromSet({ LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, }); final map = {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.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ), ); }); testWidgets('handles two keys', (WidgetTester tester) async { var 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); }); test('LogicalKeySet.hashCode is stable', () { final set1 = LogicalKeySet(LogicalKeyboardKey.keyA); expect(set1.hashCode, set1.hashCode); final set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB); expect(set2.hashCode, set2.hashCode); final set3 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, ); expect(set3.hashCode, set3.hashCode); final 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, ); }); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); final set = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.control); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(set, events.last), isTrue); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(set, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(set, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(ShortcutActivator.isActivatedBy(set, events.last), isFalse); }); test('LogicalKeySet diagnostics work.', () { final builder = DiagnosticPropertiesBuilder(); LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB).debugFillProperties(builder); final List 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 { var 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles repeated events', (WidgetTester tester) async { var 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('rejects repeated events if requested', (WidgetTester tester) async { var invoked = 0; await tester.pumpWidget( activatorTester( const SingleActivator(LogicalKeyboardKey.keyC, control: true, includeRepeats: false), (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, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async { var 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); }); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const singleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse); const noRepeatSingleActivator = SingleActivator( LogicalKeyboardKey.keyA, control: true, includeRepeats: false, ); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isTrue); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse); }); testWidgets('numLock works as expected when set to LockState.locked', ( WidgetTester tester, ) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const singleActivator = SingleActivator( LogicalKeyboardKey.numpad4, numLock: LockState.locked, ); // Lock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); // Unlock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect( HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse, ); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); }); testWidgets('numLock works as expected when set to LockState.unlocked', ( WidgetTester tester, ) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const singleActivator = SingleActivator( LogicalKeyboardKey.numpad4, numLock: LockState.unlocked, ); // Lock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); // Unlock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect( HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse, ); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); }); testWidgets('numLock works as expected when set to LockState.ignored', ( WidgetTester tester, ) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const singleActivator = SingleActivator(LogicalKeyboardKey.numpad4); // Lock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); // Unlock NumLock. await tester.sendKeyEvent(LogicalKeyboardKey.numLock); expect( HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse, ); await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4); expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4); }); group('diagnostics.', () { test('single key', () { final builder = DiagnosticPropertiesBuilder(); const SingleActivator(LogicalKeyboardKey.keyA).debugFillProperties(builder); final List 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')); }); test('no repeats', () { final builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, includeRepeats: false, ).debugFillProperties(builder); final List 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], equals('keys: Key A')); expect(description[1], equals('excluding repeats')); }); test('combination', () { final builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, control: true, shift: true, alt: true, meta: true, ).debugFillProperties(builder); final List 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 { const shortcuts = Shortcuts(shortcuts: {}, child: SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); testWidgets('Default constructed Shortcuts.manager has empty shortcuts', ( WidgetTester tester, ) async { final manager = ShortcutManager(); addTearDown(manager.dispose); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, isEmpty); final shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); testWidgets('Shortcuts.manager passes on shortcuts', (WidgetTester tester) async { final testShortcuts = { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }; final manager = ShortcutManager(shortcuts: testShortcuts); addTearDown(manager.dispose); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, equals(testShortcuts)); final shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, equals(testShortcuts)); }); testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals([LogicalKeyboardKey.shiftLeft])); }); testWidgets('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals([LogicalKeyboardKey.shiftLeft])); }); testWidgets('ShortcutManager ignores key presses with no primary focus', ( WidgetTester tester, ) async { final GlobalKey containerKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ); await tester.pump(); expect(primaryFocus, isNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isFalse); expect(pressedKeys, isEmpty); }); test('$ShortcutManager dispatches object creation in constructor', () async { await expectLater( await memoryEvents(() => ShortcutManager().dispose(), ShortcutManager), areCreateAndDispose, ); }); testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", ( WidgetTester tester, ) async { final GlobalKey containerKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( Shortcuts.manager( manager: testManager, child: Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals([LogicalKeyboardKey.shiftLeft])); }); testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', ( WidgetTester tester, ) async { final GlobalKey containerKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( MaterialApp( home: Shortcuts.manager( manager: testManager, child: Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ), ); await tester.pump(); 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 pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); addTearDown(testManager.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ); await tester.pump(); final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(handled, isFalse); expect(pressedKeys, equals([LogicalKeyboardKey.keyA])); }, ); testWidgets('Shortcuts that are bound to an action do override text fields', ( WidgetTester tester, ) async { final GlobalKey textFieldKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ); await tester.pump(); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isTrue); expect(pressedKeys, equals([LogicalKeyboardKey.keyA])); expect(invoked, isTrue); }); testWidgets('Shortcuts can override intents that apply to text fields', ( WidgetTester tester, ) async { final GlobalKey textFieldKey = GlobalKey(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Actions( actions: >{TestIntent: DoNothingAction(consumesKey: false)}, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); 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 pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); addTearDown(testManager.dispose); var invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingAndStopPropagationIntent(), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isFalse); expect(invoked, isFalse); }, ); test('Shortcuts diagnostics work.', () { final builder = DiagnosticPropertiesBuilder(); Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.keyA): const ActivateIntent(), LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right), }, child: const SizedBox(), ).debugFillProperties(builder); final List 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(direction: right)}', ), ); }); test('Shortcuts diagnostics work when debugLabel specified.', () { final builder = DiagnosticPropertiesBuilder(); Shortcuts( debugLabel: '', shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB): const ActivateIntent(), }, child: const SizedBox(), ).debugFillProperties(builder); final List 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: ')); }); test('Shortcuts diagnostics work when manager not specified.', () { final builder = DiagnosticPropertiesBuilder(); Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB): const ActivateIntent(), }, child: const SizedBox(), ).debugFillProperties(builder); final List 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: {{Key A + Key B}: ActivateIntent#00000}'), ); }); test('Shortcuts diagnostics work when manager specified.', () { final builder = DiagnosticPropertiesBuilder(); final pressedKeys = []; final testManager = TestShortcutManager( pressedKeys, shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB): const ActivateIntent(), }, ); addTearDown(testManager.dispose); Shortcuts.manager(manager: testManager, child: const SizedBox()).debugFillProperties(builder); final List 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: TestShortcutManager#00000(shortcuts: {LogicalKeySet#00000(keys: Key A + Key B): ActivateIntent#00000})', ), ); 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(LogicalKeyboardKey.space): const PrioritizedIntents( orderedIntents: [ 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: [ 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 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 { var 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; }); testWidgets('Shortcuts does not insert a semantics node when includeSemantics is false', ( WidgetTester tester, ) async { final semanticsTester = SemanticsTester(tester); addTearDown(semanticsTester.dispose); // By default, includeSemantics is true. await tester.pumpWidget( const Shortcuts(shortcuts: {}, child: SizedBox()), ); expect( semanticsTester, hasSemantics( TestSemantics.root(children: [TestSemantics(id: 1)]), ignoreRect: true, ignoreTransform: true, ), ); await tester.pumpWidget( const Shortcuts( includeSemantics: false, shortcuts: {}, child: SizedBox(), ), ); expect( semanticsTester, hasSemantics(TestSemantics.root(), ignoreRect: true, ignoreTransform: true), ); semanticsTester.dispose(); }); }); group('CharacterActivator', () { testWidgets('is triggered on events with correct character', (WidgetTester tester) async { var invoked = 0; await tester.pumpWidget( activatorTester(const CharacterActivator('?'), (Intent intent) { invoked += 1; }), ); await tester.pump(); // Press Shift + / 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 { var 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()); testWidgets('rejects repeated events if requested', (WidgetTester tester) async { var invoked = 0; await tester.pumpWidget( activatorTester(const CharacterActivator('?', includeRepeats: false), (Intent intent) { invoked += 1; }), ); await tester.pump(); // Press Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyRepeatEvent(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 Alt, Ctrl and Meta', (WidgetTester tester) async { var invoked = 0; await tester.pumpWidget( activatorTester(const CharacterActivator('?', alt: true, meta: true, control: true), ( Intent intent, ) { invoked += 1; }), ); await tester.pump(); // Press Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); expect(invoked, 0); // Press Left Alt + Ctrl + Meta + Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // Press Right Alt + Ctrl + Meta + Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.metaRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.metaRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final events = []; await tester.pumpWidget( Focus( autofocus: true, onKeyEvent: (FocusNode node, KeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const characterActivator = CharacterActivator('a'); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isTrue); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(characterActivator, events.last), isFalse); const noRepeatCharacterActivator = CharacterActivator('a', includeRepeats: false); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isTrue); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isFalse); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(noRepeatCharacterActivator, events.last), isFalse); }); group('diagnostics.', () { test('single key', () { final builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A').debugFillProperties(builder); final List 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("character: 'A'")); }); test('no repeats', () { final builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A', includeRepeats: false).debugFillProperties(builder); final List 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], equals("character: 'A'")); expect(description[1], equals('excluding repeats')); }); test('combination', () { final builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A', control: true, meta: true).debugFillProperties(builder); final List 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("character: Control + Meta + 'A'")); }); }); }); group('CallbackShortcuts', () { testWidgets('trigger on key events', (WidgetTester tester) async { var invokedA = 0; var invokedB = 0; await tester.pumpWidget( CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.keyA): () { invokedA += 1; }, const SingleActivator(LogicalKeyboardKey.keyB): () { invokedB += 1; }, }, child: const Focus(autofocus: true, child: Placeholder()), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets('nested CallbackShortcuts stop propagation', (WidgetTester tester) async { var invokedOuter = 0; var invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.keyA): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: { 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 { var invokedOuter = 0; var invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: { const CharacterActivator('b'): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: { 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 { var invokedCallbackA = 0; var invokedCallbackB = 0; var invokedActionA = 0; var invokedActionB = 0; void clear() { invokedCallbackA = 0; invokedCallbackB = 0; invokedActionA = 0; invokedActionB = 0; } await tester.pumpWidget( Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invokedActionA += 1; return true; }, ), TestIntent2: TestAction( onInvoke: (Intent intent) { invokedActionB += 1; return true; }, ), }, child: CallbackShortcuts( bindings: { const CharacterActivator('b'): () { invokedCallbackB += 1; }, }, child: Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), LogicalKeySet(LogicalKeyboardKey.keyB): const TestIntent2(), }, child: CallbackShortcuts( bindings: { 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); }); }); group('ShortcutRegistrar', () { testWidgets('trigger ShortcutRegistrar on key events', (WidgetTester tester) async { var invokedA = 0; var invokedB = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedA += 1; }), const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { invokedB += 1; }), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, child: const Focus(autofocus: true, child: Placeholder()), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets('MaterialApp has a ShortcutRegistrar listening', (WidgetTester tester) async { var invokedA = 0; var invokedB = 0; await tester.pumpWidget( MaterialApp( home: TestCallbackRegistration( shortcuts: { const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedA += 1; }), const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { invokedB += 1; }), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, child: const Focus(autofocus: true, child: Placeholder()), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets("doesn't override text field shortcuts", (WidgetTester tester) async { final controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: const { SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent( SelectionChangedCause.keyboard, ), }, child: TextField(autofocus: true, controller: controller), ), ), ), ), ); controller.text = 'Testing'; await tester.pump(); // Send a "Ctrl-A", which should be bound to select all by default. await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.pump(); expect(controller.selection.baseOffset, equals(0)); expect(controller.selection.extentOffset, equals(7)); }); testWidgets('nested ShortcutRegistrars stop propagation', (WidgetTester tester) async { var invokedOuter = 0; var invokedInner = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedOuter += 1; }), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedInner += 1; }), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, 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 ShortcutRegistrars fire appropriately', ( WidgetTester tester, ) async { var invokedOuter = 0; var invokedInner = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const CharacterActivator('b'): VoidCallbackIntent(() { invokedOuter += 1; }), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const CharacterActivator('a'): VoidCallbackIntent(() { invokedInner += 1; }), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, 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 { var invokedCallbackA = 0; var invokedCallbackB = 0; var invokedActionA = 0; var invokedActionB = 0; void clear() { invokedCallbackA = 0; invokedCallbackB = 0; invokedActionA = 0; invokedActionB = 0; } await tester.pumpWidget( Actions( actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { invokedActionA += 1; return true; }, ), TestIntent2: TestAction( onInvoke: (Intent intent) { invokedActionB += 1; return true; }, ), VoidCallbackIntent: VoidCallbackAction(), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const CharacterActivator('b'): VoidCallbackIntent(() { invokedCallbackB += 1; }), }, child: Shortcuts( shortcuts: const { SingleActivator(LogicalKeyboardKey.keyA): TestIntent(), SingleActivator(LogicalKeyboardKey.keyB): TestIntent2(), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: { const CharacterActivator('a'): VoidCallbackIntent(() { 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); }); testWidgets('Updating shortcuts triggers dependency rebuild', (WidgetTester tester) async { final shortcutsChanged = >[]; void dependenciesUpdated(Map shortcuts) { shortcutsChanged.add(shortcuts); } await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const { SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( SelectionChangedCause.keyboard, ), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, child: const Focus(autofocus: true, child: Placeholder()), ), ), ), ); await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const { SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( SelectionChangedCause.keyboard, ), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, child: const Focus(autofocus: true, child: Placeholder()), ), ), ), ); await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const { SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( SelectionChangedCause.keyboard, ), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), }, child: Actions( actions: >{VoidCallbackIntent: VoidCallbackAction()}, child: const Focus(autofocus: true, child: Placeholder()), ), ), ), ); expect(shortcutsChanged.length, equals(2)); expect( shortcutsChanged.last, equals(const { SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( SelectionChangedCause.keyboard, ), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), }), ); }); testWidgets('using a disposed token asserts', (WidgetTester tester) async { final registry = ShortcutRegistry(); addTearDown(registry.dispose); final ShortcutRegistryEntry token = registry.addAll(const { SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); token.dispose(); expect(() { token.replaceAll({}); }, throwsFlutterError); }); testWidgets('setting duplicate bindings asserts', (WidgetTester tester) async { final registry = ShortcutRegistry(); addTearDown(registry.dispose); final ShortcutRegistryEntry token = registry.addAll(const { SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); expect(() { final ShortcutRegistryEntry token2 = registry.addAll(const { SingleActivator(LogicalKeyboardKey.keyA): ActivateIntent(), }); token2.dispose(); }, throwsAssertionError); token.dispose(); }); test('dispatches object creation in constructor', () async { await expectLater( await memoryEvents(() => ShortcutRegistry().dispose(), ShortcutRegistry), areCreateAndDispose, ); }); }); } class TestCallbackRegistration extends StatefulWidget { const TestCallbackRegistration({ super.key, required this.shortcuts, this.onDependencyUpdate, required this.child, }); final Map shortcuts; final void Function(Map shortcuts)? onDependencyUpdate; final Widget child; @override State createState() => _TestCallbackRegistrationState(); } class _TestCallbackRegistrationState extends State { ShortcutRegistryEntry? _registryToken; @override void didChangeDependencies() { super.didChangeDependencies(); _registryToken?.dispose(); _registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts); } @override void didUpdateWidget(TestCallbackRegistration oldWidget) { super.didUpdateWidget(oldWidget); if (widget.shortcuts != oldWidget.shortcuts || _registryToken == null) { _registryToken?.dispose(); _registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts); } widget.onDependencyUpdate?.call(ShortcutRegistry.of(context).shortcuts); } @override void dispose() { _registryToken?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } } class TestAction extends CallbackAction { TestAction({required super.onInvoke}); } /// 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? get triggers => null; @override bool accepts(KeyEvent event, HardwareKeyboard state) { return (event is KeyDownEvent || event is KeyRepeatEvent) && 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() { var 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, {super.shortcuts}); List keys; @override KeyEventResult handleKeypress(BuildContext context, KeyEvent event) { if (event is KeyDownEvent || event is KeyRepeatEvent) { keys.add(event.logicalKey); } return super.handleKeypress(context, event); } } Widget activatorTester( ShortcutActivator activator, ValueSetter onInvoke, [ ShortcutActivator? activator2, ValueSetter? onInvoke2, ]) { final bool hasSecond = activator2 != null && onInvoke2 != null; return Actions( key: GlobalKey(), actions: >{ TestIntent: TestAction( onInvoke: (Intent intent) { onInvoke(intent); return true; }, ), if (hasSecond) TestIntent2: TestAction( onInvoke: (Intent intent) { onInvoke2(intent); return null; }, ), }, child: Shortcuts( shortcuts: { activator: const TestIntent(), if (hasSecond) activator2: const TestIntent2(), }, child: const Focus(autofocus: true, child: SizedBox(width: 100, height: 100)), ), ); }