blob: b9522292a7563496b005847b8dd7e33cb1f040c7 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '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>{PhysicalKeyboardKey.numLock}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
await simulateKeyUpEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{}));
await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{}));
await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows');
expect(HardwareKeyboard.instance.physicalKeysPressed,
equals(<PhysicalKeyboardKey>{}));
expect(HardwareKeyboard.instance.logicalKeysPressed,
equals(<LogicalKeyboardKey>{}));
expect(HardwareKeyboard.instance.lockModesEnabled,
equals(<KeyboardLockMode>{}));
}, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());
testWidgets('KeyboardManager synthesizes modifier keys in rawKeyData mode', (WidgetTester tester) async {
final List<KeyEvent> events = <KeyEvent>[];
HardwareKeyboard.instance.addHandler((KeyEvent event) {
events.add(event);
return false;
});
// While ShiftLeft is held (the event of which was skipped), press keyA.
final Map<String, dynamic> 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 = FocusNode();
final List<int> logs = <int>[];
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, <int>[1]);
logs.clear();
// Add a handler.
bool handler2Result = false;
bool handler2(KeyEvent event) {
logs.add(2);
return handler2Result;
}
HardwareKeyboard.instance.addHandler(handler2);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
false);
expect(logs, <int>[2, 1]);
logs.clear();
handler2Result = true;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
true);
expect(logs, <int>[2, 1]);
logs.clear();
// Add another handler.
handler2Result = false;
bool handler3Result = false;
bool handler3(KeyEvent event) {
logs.add(3);
return handler3Result;
}
HardwareKeyboard.instance.addHandler(handler3);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
false);
expect(logs, <int>[2, 3, 1]);
logs.clear();
handler2Result = true;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
true);
expect(logs, <int>[2, 3, 1]);
logs.clear();
handler3Result = true;
expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
true);
expect(logs, <int>[2, 3, 1]);
logs.clear();
// Add handler2 again.
HardwareKeyboard.instance.addHandler(handler2);
handler3Result = false;
handler2Result = false;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
false);
expect(logs, <int>[2, 3, 2, 1]);
logs.clear();
handler2Result = true;
expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
true);
expect(logs, <int>[2, 3, 2, 1]);
logs.clear();
// Remove handler2 once.
HardwareKeyboard.instance.removeHandler(handler2);
expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
true);
expect(logs, <int>[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 = FocusNode();
final List<KeyEvent> events = <KeyEvent>[];
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<String, dynamic> 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<KeyDownEvent>());
expect(events[1].logicalKey, LogicalKeyboardKey.shiftLeft);
expect(events[1].synthesized, false);
expect(events[2], isA<KeyUpEvent>());
expect(events[2].logicalKey, LogicalKeyboardKey.shiftLeft);
expect(events[2].synthesized, true);
expect(ServicesBinding.instance.keyboard.physicalKeysPressed, equals(<PhysicalKeyboardKey>{
PhysicalKeyboardKey.keyA,
}));
}, variant: const KeySimulatorTransitModeVariant(<KeyDataTransitMode>{
KeyDataTransitMode.rawKeyData,
}));
testWidgets('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final List<int> logs = <int>[];
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, <int>[2, 1]);
logs.clear();
}, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());
testWidgets('Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final List<String> logs = <String>[];
await tester.pumpWidget(
RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
logs.add('${event.runtimeType}');
},
child: KeyboardListener(
autofocus: true,
focusNode: focusNode,
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(<String, dynamic>{
'type': 'keydown',
'keymap': 'macos',
'keyCode': 0x00000039,
'characters': '',
'charactersIgnoringModifiers': '',
'modifiers': 0x10000,
}), equals(<String, dynamic>{'handled': false}));
expect(logs, <String>['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 List<KeyEvent> events = <KeyEvent>[];
final List<RawKeyEvent> rawEvents = <RawKeyEvent>[];
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(<String, dynamic>{
'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(<String, dynamic>{
'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<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
expect(infos['KeyMessage'], isA<DiagnosticsProperty<KeyMessage>>());
// 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<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
expect(infos['Event'], isA<DiagnosticsProperty<KeyEvent>>());
// 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 StringBuffer messages = StringBuffer();
debugPrint = (String? message, {int? wrapWidth}) {
messages.writeln(message ?? '');
};
debugPrintKeyboardEvents = true;
try {
await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
} finally {
debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents;
debugPrint = oldDebugPrint;
}
final String 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<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;
FlutterError.onError = onError;
try {
await body();
} finally {
FlutterError.onError = oldFlutterErrorOnError;
}
}
Map<String, DiagnosticsNode> _groupDiagnosticsByName(Iterable<DiagnosticsNode> infos) {
return Map<String, DiagnosticsNode>.fromIterable(
infos,
key: (dynamic node) => (node as DiagnosticsNode).name ?? '',
);
}