| // 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 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(HardwareKeyboard.instance.logicalKeysPressed, 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, |
| ); |
| }); |
| |
| testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { |
| // Collect some key events to use for testing. |
| final List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| final LogicalKeySet 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 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(HardwareKeyboard.instance.logicalKeysPressed, 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('rejects repeated events if requested', (WidgetTester tester) async { |
| int 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 { |
| 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(HardwareKeyboard.instance.logicalKeysPressed, isEmpty); |
| }); |
| |
| testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { |
| // Collect some key events to use for testing. |
| final List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| const SingleActivator 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 SingleActivator 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 List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| const SingleActivator 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 List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| const SingleActivator 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 List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| const SingleActivator 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 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')); |
| }); |
| |
| test('no repeats', () { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| const SingleActivator( |
| LogicalKeyboardKey.keyA, |
| includeRepeats: false, |
| ).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], equals('keys: Key A')); |
| expect(description[1], equals('excluding repeats')); |
| }); |
| |
| test('combination', () { |
| 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 { |
| const Shortcuts shortcuts = Shortcuts( |
| shortcuts: <LogicalKeySet, Intent>{}, |
| 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 ShortcutManager manager = ShortcutManager(); |
| addTearDown(manager.dispose); |
| expect(manager.shortcuts, isNotNull); |
| expect(manager.shortcuts, isEmpty); |
| final Shortcuts 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 Map<LogicalKeySet, Intent> testShortcuts = <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }; |
| final ShortcutManager manager = ShortcutManager(shortcuts: testShortcuts); |
| addTearDown(manager.dispose); |
| expect(manager.shortcuts, isNotNull); |
| expect(manager.shortcuts, equals(testShortcuts)); |
| final Shortcuts 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 List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; |
| final TestShortcutManager testManager = TestShortcutManager( |
| pressedKeys, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| 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>[LogicalKeyboardKey.shiftLeft])); |
| }); |
| |
| testWidgets('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async { |
| final GlobalKey containerKey = GlobalKey(); |
| final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; |
| final TestShortcutManager testManager = TestShortcutManager( |
| pressedKeys, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| 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>[LogicalKeyboardKey.shiftLeft])); |
| }); |
| |
| testWidgets('ShortcutManager ignores key presses with no primary focus', ( |
| WidgetTester tester, |
| ) async { |
| final GlobalKey containerKey = GlobalKey(); |
| final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; |
| final TestShortcutManager testManager = TestShortcutManager( |
| pressedKeys, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| Actions( |
| actions: <Type, Action<Intent>>{ |
| 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 List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; |
| final TestShortcutManager testManager = TestShortcutManager( |
| pressedKeys, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| Shortcuts.manager( |
| manager: testManager, |
| 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(); |
| 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, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Shortcuts.manager( |
| manager: testManager, |
| 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(); |
| 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, |
| shortcuts: <LogicalKeySet, Intent>{ |
| 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>[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, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Shortcuts.manager( |
| manager: testManager, |
| child: Actions( |
| actions: <Type, Action<Intent>>{ |
| 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>[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, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Shortcuts.manager( |
| manager: testManager, |
| 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(); |
| 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, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| bool invoked = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Shortcuts.manager( |
| manager: testManager, |
| child: Actions( |
| actions: <Type, Action<Intent>>{ |
| TestIntent: TestAction( |
| onInvoke: (Intent intent) { |
| invoked = true; |
| return invoked; |
| }, |
| ), |
| }, |
| child: Shortcuts( |
| shortcuts: <LogicalKeySet, Intent>{ |
| 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 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(direction: right)}', |
| ), |
| ); |
| }); |
| |
| 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 not specified.', () { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| Shortcuts( |
| 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], |
| equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'), |
| ); |
| }); |
| |
| test('Shortcuts diagnostics work when manager specified.', () { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; |
| final TestShortcutManager testManager = TestShortcutManager( |
| pressedKeys, |
| shortcuts: <LogicalKeySet, Intent>{ |
| LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB): const ActivateIntent(), |
| }, |
| ); |
| addTearDown(testManager.dispose); |
| |
| Shortcuts.manager(manager: testManager, 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: 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, 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; |
| }); |
| |
| testWidgets('Shortcuts does not insert a semantics node when includeSemantics is false', ( |
| WidgetTester tester, |
| ) async { |
| final SemanticsTester semanticsTester = SemanticsTester(tester); |
| addTearDown(semanticsTester.dispose); |
| |
| // By default, includeSemantics is true. |
| await tester.pumpWidget( |
| const Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox()), |
| ); |
| |
| expect( |
| semanticsTester, |
| hasSemantics( |
| TestSemantics.root(children: <TestSemantics>[TestSemantics(id: 1)]), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| const Shortcuts( |
| includeSemantics: false, |
| shortcuts: <LogicalKeySet, Intent>{}, |
| 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 { |
| int 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 { |
| 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()); |
| |
| testWidgets('rejects repeated events if requested', (WidgetTester tester) async { |
| int 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 { |
| int 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 List<KeyEvent> events = <KeyEvent>[]; |
| await tester.pumpWidget( |
| Focus( |
| autofocus: true, |
| onKeyEvent: (FocusNode node, KeyEvent event) { |
| events.add(event); |
| return KeyEventResult.ignored; |
| }, |
| child: const SizedBox(), |
| ), |
| ); |
| |
| const CharacterActivator 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 CharacterActivator 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 DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| const CharacterActivator('A').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("character: 'A'")); |
| }); |
| |
| test('no repeats', () { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| const CharacterActivator('A', includeRepeats: false).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], equals("character: 'A'")); |
| expect(description[1], equals('excluding repeats')); |
| }); |
| |
| test('combination', () { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| |
| const CharacterActivator('A', control: 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("character: Control + Meta + 'A'")); |
| }); |
| }); |
| }); |
| |
| group('CallbackShortcuts', () { |
| testWidgets('trigger on key events', (WidgetTester tester) async { |
| int invokedA = 0; |
| int invokedB = 0; |
| await tester.pumpWidget( |
| CallbackShortcuts( |
| bindings: <ShortcutActivator, VoidCallback>{ |
| 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 { |
| 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); |
| }); |
| }); |
| |
| group('ShortcutRegistrar', () { |
| testWidgets('trigger ShortcutRegistrar on key events', (WidgetTester tester) async { |
| int invokedA = 0; |
| int invokedB = 0; |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { |
| invokedA += 1; |
| }), |
| const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { |
| invokedB += 1; |
| }), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{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 { |
| int invokedA = 0; |
| int invokedB = 0; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { |
| invokedA += 1; |
| }), |
| const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { |
| invokedB += 1; |
| }), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{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 TextEditingController controller = TextEditingController(); |
| addTearDown(controller.dispose); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: const <ShortcutActivator, Intent>{ |
| 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 { |
| int invokedOuter = 0; |
| int invokedInner = 0; |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { |
| invokedOuter += 1; |
| }), |
| }, |
| child: ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { |
| invokedInner += 1; |
| }), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{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 { |
| int invokedOuter = 0; |
| int invokedInner = 0; |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const CharacterActivator('b'): VoidCallbackIntent(() { |
| invokedOuter += 1; |
| }), |
| }, |
| child: ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const CharacterActivator('a'): VoidCallbackIntent(() { |
| invokedInner += 1; |
| }), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{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 { |
| 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; |
| }, |
| ), |
| VoidCallbackIntent: VoidCallbackAction(), |
| }, |
| child: ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| const CharacterActivator('b'): VoidCallbackIntent(() { |
| invokedCallbackB += 1; |
| }), |
| }, |
| child: Shortcuts( |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): TestIntent(), |
| SingleActivator(LogicalKeyboardKey.keyB): TestIntent2(), |
| }, |
| child: ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| shortcuts: <ShortcutActivator, Intent>{ |
| 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 List<Map<ShortcutActivator, Intent>> shortcutsChanged = |
| <Map<ShortcutActivator, Intent>>[]; |
| void dependenciesUpdated(Map<ShortcutActivator, Intent> shortcuts) { |
| shortcutsChanged.add(shortcuts); |
| } |
| |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| onDependencyUpdate: dependenciesUpdated, |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( |
| SelectionChangedCause.keyboard, |
| ), |
| SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{VoidCallbackIntent: VoidCallbackAction()}, |
| child: const Focus(autofocus: true, child: Placeholder()), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| onDependencyUpdate: dependenciesUpdated, |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( |
| SelectionChangedCause.keyboard, |
| ), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{VoidCallbackIntent: VoidCallbackAction()}, |
| child: const Focus(autofocus: true, child: Placeholder()), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| ShortcutRegistrar( |
| child: TestCallbackRegistration( |
| onDependencyUpdate: dependenciesUpdated, |
| shortcuts: const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( |
| SelectionChangedCause.keyboard, |
| ), |
| SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), |
| }, |
| child: Actions( |
| actions: <Type, Action<Intent>>{VoidCallbackIntent: VoidCallbackAction()}, |
| child: const Focus(autofocus: true, child: Placeholder()), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(shortcutsChanged.length, equals(2)); |
| expect( |
| shortcutsChanged.last, |
| equals(const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent( |
| SelectionChangedCause.keyboard, |
| ), |
| SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), |
| }), |
| ); |
| }); |
| |
| testWidgets('using a disposed token asserts', (WidgetTester tester) async { |
| final ShortcutRegistry registry = ShortcutRegistry(); |
| addTearDown(registry.dispose); |
| final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), |
| }); |
| token.dispose(); |
| expect(() { |
| token.replaceAll(<ShortcutActivator, Intent>{}); |
| }, throwsFlutterError); |
| }); |
| |
| testWidgets('setting duplicate bindings asserts', (WidgetTester tester) async { |
| final ShortcutRegistry registry = ShortcutRegistry(); |
| addTearDown(registry.dispose); |
| final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ |
| SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), |
| }); |
| expect(() { |
| final ShortcutRegistryEntry token2 = registry.addAll(const <ShortcutActivator, Intent>{ |
| 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<ShortcutActivator, Intent> shortcuts; |
| final void Function(Map<ShortcutActivator, Intent> shortcuts)? onDependencyUpdate; |
| final Widget child; |
| |
| @override |
| State<TestCallbackRegistration> createState() => _TestCallbackRegistrationState(); |
| } |
| |
| class _TestCallbackRegistrationState extends State<TestCallbackRegistration> { |
| 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<Intent> { |
| 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<LogicalKeyboardKey>? 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() { |
| 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, {super.shortcuts}); |
| |
| List<LogicalKeyboardKey> 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<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); |
| return null; |
| }, |
| ), |
| }, |
| child: Shortcuts( |
| shortcuts: <ShortcutActivator, Intent>{ |
| activator: const TestIntent(), |
| if (hasSecond) activator2: const TestIntent2(), |
| }, |
| child: const Focus(autofocus: true, child: SizedBox(width: 100, height: 100)), |
| ), |
| ); |
| } |