Unverified Commit 0e6cb28d authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add fake keyboard key generation to the testing framework (#40706)

There were four or five different implementations in various tests for sendFakeKeyEvent, which roughly all did the same thing. I was going to add yet another one, and decided that it needed to be generalized and centralized. This replaces those instances with something that just takes a LogicalKeyboardKey so that it's self-documenting, and can be used with multiple platforms.

This adds two functions to widget tester: sendKeyDownEvent and sendKeyUpEvent which simulate key up/down from a physical keyboard. It also adds global functions simulateKeyDownEvent and simulateKeyUpEvent that can be called without a widget tester. All are async functions protected by the async guard.
parent 4815b26d
...@@ -17,6 +17,7 @@ export 'src/services/clipboard.dart'; ...@@ -17,6 +17,7 @@ export 'src/services/clipboard.dart';
export 'src/services/font_loader.dart'; export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart'; export 'src/services/haptic_feedback.dart';
export 'src/services/keyboard_key.dart'; export 'src/services/keyboard_key.dart';
export 'src/services/keyboard_maps.dart';
export 'src/services/message_codec.dart'; export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart'; export 'src/services/message_codecs.dart';
export 'src/services/platform_channel.dart'; export 'src/services/platform_channel.dart';
......
...@@ -512,4 +512,12 @@ class RawKeyboard { ...@@ -512,4 +512,12 @@ class RawKeyboard {
Set<LogicalKeyboardKey> get keysPressed { Set<LogicalKeyboardKey> get keysPressed {
return _keysPressed.toSet(); return _keysPressed.toSet();
} }
/// Clears the list of keys returned from [keysPressed].
///
/// This is used by the testing framework to make sure tests are hermetic.
@visibleForTesting
void clearKeysPressed() {
_keysPressed.clear();
}
} }
...@@ -2,22 +2,13 @@ ...@@ -2,22 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void sendFakeKeyEvent(Map<String, dynamic> data) {
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) {},
);
}
void main() { void main() {
final GlobalKey widgetKey = GlobalKey(); final GlobalKey widgetKey = GlobalKey();
Future<BuildContext> setupWidget(WidgetTester tester) async { Future<BuildContext> setupWidget(WidgetTester tester) async {
...@@ -426,15 +417,9 @@ void main() { ...@@ -426,15 +417,9 @@ void main() {
return false; return false;
} }
void sendEvent() { Future<void> sendEvent() async {
receivedAnEvent.clear(); receivedAnEvent.clear();
sendFakeKeyEvent(<String, dynamic>{ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
} }
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
...@@ -465,21 +450,21 @@ void main() { ...@@ -465,21 +450,21 @@ void main() {
child4.requestFocus(); child4.requestFocus();
await tester.pump(); await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4}); shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent(); await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{child4})); expect(receivedAnEvent, equals(<FocusNode>{child4}));
shouldHandle.remove(child4); shouldHandle.remove(child4);
sendEvent(); await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{parent2})); expect(receivedAnEvent, equals(<FocusNode>{parent2}));
shouldHandle.remove(parent2); shouldHandle.remove(parent2);
sendEvent(); await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{scope2})); expect(receivedAnEvent, equals(<FocusNode>{scope2}));
shouldHandle.clear(); shouldHandle.clear();
sendEvent(); await sendEvent();
expect(receivedAnEvent, isEmpty); expect(receivedAnEvent, isEmpty);
child1.requestFocus(); child1.requestFocus();
await tester.pump(); await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4}); shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent(); await sendEvent();
// Since none of the focused nodes handle this event, nothing should // Since none of the focused nodes handle this event, nothing should
// receive it. // receive it.
expect(receivedAnEvent, isEmpty); expect(receivedAnEvent, isEmpty);
...@@ -500,13 +485,7 @@ void main() { ...@@ -500,13 +485,7 @@ void main() {
expect(lastMode, isNull); expect(lastMode, isNull);
focusManager.highlightStrategy = FocusHighlightStrategy.automatic; focusManager.highlightStrategy = FocusHighlightStrategy.automatic;
expect(focusManager.highlightMode, equals(FocusHighlightMode.touch)); expect(focusManager.highlightMode, equals(FocusHighlightMode.touch));
sendFakeKeyEvent(<String, dynamic>{ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
expect(callCount, equals(1)); expect(callCount, equals(1));
expect(lastMode, FocusHighlightMode.traditional); expect(lastMode, FocusHighlightMode.traditional);
expect(focusManager.highlightMode, equals(FocusHighlightMode.traditional)); expect(focusManager.highlightMode, equals(FocusHighlightMode.traditional));
......
...@@ -2,20 +2,10 @@ ...@@ -2,20 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void sendFakeKeyEvent(Map<String, dynamic> data) {
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) {},
);
}
void main() { void main() {
testWidgets('Can dispose without keyboard', (WidgetTester tester) async { testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
...@@ -40,21 +30,15 @@ void main() { ...@@ -40,21 +30,15 @@ void main() {
focusNode.requestFocus(); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.idle(); await tester.idle();
expect(events.length, 1); expect(events.length, 2);
expect(events[0].runtimeType, equals(RawKeyDownEvent)); expect(events[0].runtimeType, equals(RawKeyDownEvent));
expect(events[0].data.runtimeType, equals(RawKeyEventDataFuchsia)); expect(events[0].data.runtimeType, equals(RawKeyEventDataFuchsia));
final RawKeyEventDataFuchsia typedData = events[0].data; final RawKeyEventDataFuchsia typedData = events[0].data;
expect(typedData.hidUsage, 0x04); expect(typedData.hidUsage, 0x700e3);
expect(typedData.codePoint, 0x64); expect(typedData.codePoint, 0x0);
expect(typedData.modifiers, RawKeyEventDataFuchsia.modifierLeftMeta); expect(typedData.modifiers, RawKeyEventDataFuchsia.modifierLeftMeta);
expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue); expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue);
...@@ -78,27 +62,15 @@ void main() { ...@@ -78,27 +62,15 @@ void main() {
focusNode.requestFocus(); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.idle(); await tester.idle();
expect(events.length, 1); expect(events.length, 2);
events.clear(); events.clear();
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
sendFakeKeyEvent(<String, dynamic>{ await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.idle(); await tester.idle();
......
...@@ -6,18 +6,9 @@ import 'package:flutter/foundation.dart'; ...@@ -6,18 +6,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/src/services/keyboard_key.dart'; import 'package:flutter/src/services/keyboard_key.dart';
import 'package:flutter/src/services/keyboard_maps.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void sendFakeKeyEvent(Map<String, dynamic> data) {
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) {},
);
}
typedef PostInvokeCallback = void Function({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher}); typedef PostInvokeCallback = void Function({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher});
class TestAction extends CallbackAction { class TestAction extends CallbackAction {
...@@ -74,34 +65,6 @@ class TestShortcutManager extends ShortcutManager { ...@@ -74,34 +65,6 @@ class TestShortcutManager extends ShortcutManager {
} }
} }
void testKeypress(LogicalKeyboardKey key) {
assert(key.debugName != null);
int keyCode;
kAndroidToLogicalKey.forEach((int code, LogicalKeyboardKey codeKey) {
if (key == codeKey) {
keyCode = code;
}
});
assert(keyCode != null, 'Key $key not found in Android key map');
int scanCode;
kAndroidToPhysicalKey.forEach((int code, PhysicalKeyboardKey codeKey) {
if (key.debugName == codeKey.debugName) {
scanCode = code;
}
});
assert(scanCode != null, 'Physical key for $key not found in Android key map');
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'android',
'keyCode': keyCode,
'plainCodePoint': 0,
'codePoint': 0,
'character': null,
'scanCode': scanCode,
'metaState': 0,
});
}
void main() { void main() {
group(LogicalKeySet, () { group(LogicalKeySet, () {
test('$LogicalKeySet passes parameters correctly.', () { test('$LogicalKeySet passes parameters correctly.', () {
...@@ -248,7 +211,7 @@ void main() { ...@@ -248,7 +211,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(containerKey.currentContext), isNotNull); expect(Shortcuts.of(containerKey.currentContext), isNotNull);
testKeypress(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue); expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
}); });
...@@ -284,7 +247,7 @@ void main() { ...@@ -284,7 +247,7 @@ void main() {
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(containerKey.currentContext), isNotNull); expect(Shortcuts.of(containerKey.currentContext), isNotNull);
testKeypress(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue); expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
}); });
......
...@@ -51,6 +51,7 @@ export 'src/accessibility.dart'; ...@@ -51,6 +51,7 @@ export 'src/accessibility.dart';
export 'src/all_elements.dart'; export 'src/all_elements.dart';
export 'src/binding.dart'; export 'src/binding.dart';
export 'src/controller.dart'; export 'src/controller.dart';
export 'src/event_simulation.dart';
export 'src/finders.dart'; export 'src/finders.dart';
export 'src/goldens.dart'; export 'src/goldens.dart';
export 'src/matchers.dart'; export 'src/matchers.dart';
......
...@@ -796,6 +796,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -796,6 +796,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
_pendingExceptionDetails = null; _pendingExceptionDetails = null;
_parentZone = null; _parentZone = null;
buildOwner.focusManager = FocusManager(); buildOwner.focusManager = FocusManager();
// Disabling the warning because @visibleForTesting doesn't take the testing
// framework itself into account, but we don't want it visible outside of
// tests.
// ignore: invalid_use_of_visible_for_testing_member
RawKeyboard.instance.clearKeysPressed();
assert(!RendererBinding.instance.mouseTracker.mouseIsConnected, assert(!RendererBinding.instance.mouseTracker.mouseIsConnected,
'The MouseTracker thinks that there is still a mouse connected, which indicates that a ' 'The MouseTracker thinks that there is still a mouse connected, which indicates that a '
'test has not removed the mouse pointer which it added. Call removePointer on the ' 'test has not removed the mouse pointer which it added. Call removePointer on the '
......
This diff is collapsed.
...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:test_api/test_api.dart' as test_package; import 'package:test_api/test_api.dart' as test_package;
...@@ -16,6 +17,7 @@ import 'package:test_api/test_api.dart' as test_package; ...@@ -16,6 +17,7 @@ import 'package:test_api/test_api.dart' as test_package;
import 'all_elements.dart'; import 'all_elements.dart';
import 'binding.dart'; import 'binding.dart';
import 'controller.dart'; import 'controller.dart';
import 'event_simulation.dart';
import 'finders.dart'; import 'finders.dart';
import 'matchers.dart'; import 'matchers.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
...@@ -719,6 +721,74 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -719,6 +721,74 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}); });
} }
/// Simulates sending physical key down and up events through the system channel.
///
/// This only simulates key events coming from a physical keyboard, not from a
/// soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
/// This method sends both the key down and the key up events, to simulate a
/// key press. To simulate individual down and/or up events, see
/// [sendKeyDownEvent] and [sendKeyUpEvent].
///
/// See also:
///
/// - [sendKeyDownEvent] to simulate only a key down event.
/// - [sendKeyUpEvent] to simulate only a key up event.
Future<void> sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
await simulateKeyDownEvent(key, platform: platform);
// Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform);
}
/// Simulates sending a physical key down event through the system channel.
///
/// This only simulates key down events coming from a physical keyboard, not
/// from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". Must not be null. Some platforms (e.g.
/// Windows, iOS) are not yet supported.
///
/// Keys that are down when the test completes are cleared after each test.
///
/// See also:
///
/// - [sendKeyUpEvent] to simulate the corresponding key up event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<void> sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyDownEvent(key, platform: platform);
}
/// Simulates sending a physical key up event through the system channel.
///
/// This only simulates key up events coming from a physical keyboard,
/// not from a soft keyboard.
///
/// Specify `platform` as one of the platforms allowed in
/// [Platform.operatingSystem] to make the event appear to be from that type
/// of system. Defaults to "android". May not be null.
///
/// See also:
///
/// - [sendKeyDownEvent] to simulate the corresponding key down event.
/// - [sendKeyEvent] to simulate both the key up and key down in the same call.
Future<void> sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async {
assert(platform != null);
// Internally wrapped in async guard.
return simulateKeyUpEvent(key, platform: platform);
}
/// Makes an effort to dismiss the current page with a Material [Scaffold] or /// Makes an effort to dismiss the current page with a Material [Scaffold] or
/// a [CupertinoPageScaffold]. /// a [CupertinoPageScaffold].
/// ///
......
// Copyright 2019 The Chromium 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/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
const List<String> platforms = <String>['linux', 'macos', 'android', 'fuchsia'];
void main() {
testWidgets('simulates keyboard events', (WidgetTester tester) async {
final List<RawKeyEvent> events = <RawKeyEvent>[];
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
RawKeyboardListener(
focusNode: focusNode,
onKey: events.add,
child: Container(),
),
);
focusNode.requestFocus();
await tester.idle();
for (String platform in platforms) {
await tester.sendKeyEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
await tester.sendKeyEvent(LogicalKeyboardKey.shift, platform: platform);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA, platform: platform);
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA, platform: platform);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1, platform: platform);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad1, platform: platform);
await tester.idle();
expect(events.length, 8);
for (int i = 0; i < events.length; ++i) {
final bool isEven = i % 2 == 0;
if (isEven) {
expect(events[i].runtimeType, equals(RawKeyDownEvent));
} else {
expect(events[i].runtimeType, equals(RawKeyUpEvent));
}
if (i < 4) {
expect(events[i].data.isModifierPressed(ModifierKey.shiftModifier, side: KeyboardSide.left), equals(isEven));
}
}
events.clear();
}
await tester.pumpWidget(Container());
focusNode.dispose();
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment