// 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 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets( 'HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async { await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numLock}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numLock}), ); expect( HardwareKeyboard.instance.lockModesEnabled, equals({KeyboardLockMode.numLock}), ); await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.lockModesEnabled, equals({KeyboardLockMode.numLock}), ); await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.lockModesEnabled, equals({KeyboardLockMode.numLock}), ); await simulateKeyUpEvent(LogicalKeyboardKey.numLock); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.lockModesEnabled, equals({KeyboardLockMode.numLock}), ); await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}), ); expect(HardwareKeyboard.instance.lockModesEnabled, equals({})); await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect( HardwareKeyboard.instance.physicalKeysPressed, equals({PhysicalKeyboardKey.numLock}), ); expect( HardwareKeyboard.instance.logicalKeysPressed, equals({LogicalKeyboardKey.numLock}), ); expect(HardwareKeyboard.instance.lockModesEnabled, equals({})); await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect(HardwareKeyboard.instance.physicalKeysPressed, equals({})); expect(HardwareKeyboard.instance.logicalKeysPressed, equals({})); expect(HardwareKeyboard.instance.lockModesEnabled, equals({})); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData(), ); testWidgets( 'KeyEvent can tell which keys are pressed', (WidgetTester tester) async { await tester.pumpWidget(const Focus(autofocus: true, child: SizedBox())); await tester.pump(); await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isTrue); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isTrue); await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue); await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue); await simulateKeyUpEvent(LogicalKeyboardKey.numLock); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue); await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isTrue); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isTrue); await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isFalse); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isFalse); await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows'); expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isFalse); expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isFalse); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData(), ); testWidgets('KeyboardManager synthesizes modifier keys in rawKeyData mode', ( WidgetTester tester, ) async { final events = []; HardwareKeyboard.instance.addHandler((KeyEvent event) { events.add(event); return false; }); // While ShiftLeft is held (the event of which was skipped), press keyA. final Map rawMessage = kIsWeb ? (KeyEventSimulator.getKeyData(LogicalKeyboardKey.keyA, platform: 'web') ..['metaState'] = RawKeyEventDataWeb.modifierShift) : (KeyEventSimulator.getKeyData(LogicalKeyboardKey.keyA, platform: 'android') ..['metaState'] = RawKeyEventDataAndroid.modifierLeftShift | RawKeyEventDataAndroid.modifierShift); tester.binding.keyEventManager.handleRawKeyMessage(rawMessage); expect(events, hasLength(2)); expect(events[0].physicalKey, PhysicalKeyboardKey.shiftLeft); expect(events[0].logicalKey, LogicalKeyboardKey.shiftLeft); expect(events[0].synthesized, true); expect(events[1].physicalKey, PhysicalKeyboardKey.keyA); expect(events[1].logicalKey, LogicalKeyboardKey.keyA); expect(events[1].synthesized, false); }); testWidgets('Dispatch events to all handlers', (WidgetTester tester) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); final logs = []; await tester.pumpWidget( KeyboardListener( autofocus: true, focusNode: focusNode, child: Container(), onKeyEvent: (KeyEvent event) { logs.add(1); }, ), ); // Only the Service binding handler. expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA), false); expect(logs, [1]); logs.clear(); // Add a handler. var handler2Result = false; bool handler2(KeyEvent event) { logs.add(2); return handler2Result; } HardwareKeyboard.instance.addHandler(handler2); expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA), false); expect(logs, [2, 1]); logs.clear(); handler2Result = true; expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA), true); expect(logs, [2, 1]); logs.clear(); // Add another handler. handler2Result = false; var handler3Result = false; bool handler3(KeyEvent event) { logs.add(3); return handler3Result; } HardwareKeyboard.instance.addHandler(handler3); expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA), false); expect(logs, [2, 3, 1]); logs.clear(); handler2Result = true; expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA), true); expect(logs, [2, 3, 1]); logs.clear(); handler3Result = true; expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA), true); expect(logs, [2, 3, 1]); logs.clear(); // Add handler2 again. HardwareKeyboard.instance.addHandler(handler2); handler3Result = false; handler2Result = false; expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA), false); expect(logs, [2, 3, 2, 1]); logs.clear(); handler2Result = true; expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA), true); expect(logs, [2, 3, 2, 1]); logs.clear(); // Remove handler2 once. HardwareKeyboard.instance.removeHandler(handler2); expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA), true); expect(logs, [3, 2, 1]); logs.clear(); }, variant: KeySimulatorTransitModeVariant.all()); // Regression test for https://github.com/flutter/flutter/issues/99196 . // // In rawKeyData mode, if a key down event is dispatched but immediately // synthesized to be released, the old logic would trigger a Null check // _CastError on _hardwareKeyboard.lookUpLayout(key). The original scenario // that this is triggered on Android is unknown. Here we make up a scenario // where a ShiftLeft key down is dispatched but the modifier bit is not set. testWidgets( 'Correctly convert down events that are synthesized released', (WidgetTester tester) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); final events = []; await tester.pumpWidget( KeyboardListener( autofocus: true, focusNode: focusNode, child: Container(), onKeyEvent: (KeyEvent event) { events.add(event); }, ), ); // Dispatch an arbitrary event to bypass the pressedKeys check. await simulateKeyDownEvent(LogicalKeyboardKey.keyA, platform: 'web'); // Dispatch an final Map data2 = KeyEventSimulator.getKeyData( LogicalKeyboardKey.shiftLeft, platform: 'web', )..['metaState'] = 0; await tester.binding.defaultBinaryMessenger.handlePlatformMessage( SystemChannels.keyEvent.name, SystemChannels.keyEvent.codec.encodeMessage(data2), (ByteData? data) {}, ); expect(events, hasLength(3)); expect(events[1], isA()); expect(events[1].logicalKey, LogicalKeyboardKey.shiftLeft); expect(events[1].synthesized, false); expect(events[2], isA()); expect(events[2].logicalKey, LogicalKeyboardKey.shiftLeft); expect(events[2].synthesized, true); expect( ServicesBinding.instance.keyboard.physicalKeysPressed, equals({PhysicalKeyboardKey.keyA}), ); }, variant: const KeySimulatorTransitModeVariant({ KeyDataTransitMode.rawKeyData, }), ); testWidgets( 'Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async { final focusNode = FocusNode(); addTearDown(focusNode.dispose); final logs = []; await tester.pumpWidget( KeyboardListener( autofocus: true, focusNode: focusNode, child: Container(), onKeyEvent: (KeyEvent event) { logs.add(1); }, ), ); ServicesBinding.instance.keyboard.addHandler((KeyEvent event) { logs.add(2); return false; }); // Dispatch a solitary synthesized event. expect( ServicesBinding.instance.keyEventManager.handleKeyData( ui.KeyData( timeStamp: Duration.zero, type: ui.KeyEventType.down, logical: LogicalKeyboardKey.keyA.keyId, physical: PhysicalKeyboardKey.keyA.usbHidUsage, character: null, synthesized: true, ), ), false, ); expect(logs, [2, 1]); logs.clear(); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData(), ); testWidgets( 'Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async { final keyboardListenerFocusNode = FocusNode(); addTearDown(keyboardListenerFocusNode.dispose); final rawKeyboardListenerFocusNode = FocusNode(); addTearDown(rawKeyboardListenerFocusNode.dispose); final logs = []; await tester.pumpWidget( RawKeyboardListener( focusNode: rawKeyboardListenerFocusNode, onKey: (RawKeyEvent event) { logs.add('${event.runtimeType}'); }, child: KeyboardListener( autofocus: true, focusNode: keyboardListenerFocusNode, child: Container(), onKeyEvent: (KeyEvent event) { logs.add('${event.runtimeType}'); }, ), ), ); // On macOS, a CapsLock tap yields a down event and a synthesized up event. expect( ServicesBinding.instance.keyEventManager.handleKeyData( ui.KeyData( timeStamp: Duration.zero, type: ui.KeyEventType.down, logical: LogicalKeyboardKey.capsLock.keyId, physical: PhysicalKeyboardKey.capsLock.usbHidUsage, character: null, synthesized: false, ), ), false, ); expect( ServicesBinding.instance.keyEventManager.handleKeyData( ui.KeyData( timeStamp: Duration.zero, type: ui.KeyEventType.up, logical: LogicalKeyboardKey.capsLock.keyId, physical: PhysicalKeyboardKey.capsLock.usbHidUsage, character: null, synthesized: true, ), ), false, ); expect( await ServicesBinding.instance.keyEventManager.handleRawKeyMessage({ 'type': 'keydown', 'keymap': 'macos', 'keyCode': 0x00000039, 'characters': '', 'charactersIgnoringModifiers': '', 'modifiers': 0x10000, }), equals({'handled': false}), ); expect(logs, ['RawKeyDownEvent', 'KeyDownEvent', 'KeyUpEvent']); logs.clear(); }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData(), ); // The first key data received from the engine might be an empty key data. // In that case, the key data should not be converted to any [KeyEvent]s, // but is only used so that *a* key data comes before the raw key message // and makes [KeyEventManager] infer [KeyDataTransitMode.keyDataThenRawKeyData]. testWidgets('Empty keyData yields no event but triggers inference', (WidgetTester tester) async { final events = []; final rawEvents = []; tester.binding.keyboard.addHandler((KeyEvent event) { events.add(event); return true; }); RawKeyboard.instance.addListener((RawKeyEvent event) { rawEvents.add(event); }); tester.binding.keyEventManager.handleKeyData( const ui.KeyData( type: ui.KeyEventType.down, timeStamp: Duration.zero, logical: 0, physical: 0, character: 'a', synthesized: false, ), ); tester.binding.keyEventManager.handleRawKeyMessage({ 'type': 'keydown', 'keymap': 'windows', 'keyCode': 0x04, 'scanCode': 0x04, 'characterCodePoint': 0, 'modifiers': 0, }); expect(events.length, 0); expect(rawEvents.length, 1); // Dispatch another key data to ensure it's in // [KeyDataTransitMode.keyDataThenRawKeyData] mode (otherwise assertion // will be thrown upon a KeyData). tester.binding.keyEventManager.handleKeyData( const ui.KeyData( type: ui.KeyEventType.down, timeStamp: Duration.zero, logical: 0x22, physical: 0x70034, character: '"', synthesized: false, ), ); tester.binding.keyEventManager.handleRawKeyMessage({ 'type': 'keydown', 'keymap': 'windows', 'keyCode': 0x04, 'scanCode': 0x04, 'characterCodePoint': 0, 'modifiers': 0, }); expect(events.length, 1); expect(rawEvents.length, 2); }); testWidgets('Exceptions from keyMessageHandler are caught and reported', ( WidgetTester tester, ) async { final KeyMessageHandler? oldKeyMessageHandler = tester.binding.keyEventManager.keyMessageHandler; addTearDown(() { tester.binding.keyEventManager.keyMessageHandler = oldKeyMessageHandler; }); // When keyMessageHandler throws an error... tester.binding.keyEventManager.keyMessageHandler = (KeyMessage message) { throw 1; }; // Simulate a key down event. FlutterErrorDetails? record; await _runWhileOverridingOnError( () => simulateKeyDownEvent(LogicalKeyboardKey.keyA), onError: (FlutterErrorDetails details) { record = details; }, ); // ... the error should be caught. expect(record, isNotNull); expect(record!.exception, 1); final Map infos = _groupDiagnosticsByName( record!.informationCollector!(), ); expect(infos['KeyMessage'], isA>()); // But the exception should not interrupt recording the state. // Now the keyMessageHandler no longer throws an error. tester.binding.keyEventManager.keyMessageHandler = null; record = null; // Simulate a key up event. await _runWhileOverridingOnError( () => simulateKeyUpEvent(LogicalKeyboardKey.keyA), onError: (FlutterErrorDetails details) { record = details; }, ); // If the previous state (key down) wasn't recorded, this key up event will // trigger assertions. expect(record, isNull); }); testWidgets( 'Exceptions from HardwareKeyboard handlers are caught and reported', (WidgetTester tester) async { bool throwingCallback(KeyEvent event) { throw 1; } // When the handler throws an error... HardwareKeyboard.instance.addHandler(throwingCallback); // Simulate a key down event. FlutterErrorDetails? record; await _runWhileOverridingOnError( () => simulateKeyDownEvent(LogicalKeyboardKey.keyA), onError: (FlutterErrorDetails details) { record = details; }, ); // ... the error should be caught. expect(record, isNotNull); expect(record!.exception, 1); final Map infos = _groupDiagnosticsByName( record!.informationCollector!(), ); expect(infos['Event'], isA>()); // But the exception should not interrupt recording the state. // Now the key handler no longer throws an error. HardwareKeyboard.instance.removeHandler(throwingCallback); record = null; // Simulate a key up event. await _runWhileOverridingOnError( () => simulateKeyUpEvent(LogicalKeyboardKey.keyA), onError: (FlutterErrorDetails details) { record = details; }, ); // If the previous state (key down) wasn't recorded, this key up event will // trigger assertions. expect(record, isNull); }, variant: KeySimulatorTransitModeVariant.all(), ); testWidgets('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async { final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents; final DebugPrintCallback oldDebugPrint = debugPrint; final messages = StringBuffer(); debugPrint = (String? message, {int? wrapWidth}) { messages.writeln(message ?? ''); }; debugPrintKeyboardEvents = true; try { await simulateKeyDownEvent(LogicalKeyboardKey.keyA); } finally { debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents; debugPrint = oldDebugPrint; } final messagesStr = messages.toString(); expect(messagesStr, contains('KEYBOARD: Key event received: ')); expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:')); expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:')); }); } Future _runWhileOverridingOnError( AsyncCallback body, { required FlutterExceptionHandler onError, }) async { final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError; FlutterError.onError = onError; try { await body(); } finally { FlutterError.onError = oldFlutterErrorOnError; } } Map _groupDiagnosticsByName(Iterable infos) { return Map.fromIterable( infos, key: (dynamic node) => (node as DiagnosticsNode).name ?? '', ); }