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';
export 'src/services/font_loader.dart';
export 'src/services/haptic_feedback.dart';
export 'src/services/keyboard_key.dart';
export 'src/services/keyboard_maps.dart';
export 'src/services/message_codec.dart';
export 'src/services/message_codecs.dart';
export 'src/services/platform_channel.dart';
......
......@@ -512,4 +512,12 @@ class RawKeyboard {
Set<LogicalKeyboardKey> get keysPressed {
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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.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() {
final GlobalKey widgetKey = GlobalKey();
Future<BuildContext> setupWidget(WidgetTester tester) async {
......@@ -426,15 +417,9 @@ void main() {
return false;
}
void sendEvent() {
Future<void> sendEvent() async {
receivedAnEvent.clear();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
}
final BuildContext context = await setupWidget(tester);
......@@ -465,21 +450,21 @@ void main() {
child4.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent();
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{child4}));
shouldHandle.remove(child4);
sendEvent();
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{parent2}));
shouldHandle.remove(parent2);
sendEvent();
await sendEvent();
expect(receivedAnEvent, equals(<FocusNode>{scope2}));
shouldHandle.clear();
sendEvent();
await sendEvent();
expect(receivedAnEvent, isEmpty);
child1.requestFocus();
await tester.pump();
shouldHandle.addAll(<FocusNode>{scope2, parent2, child2, child4});
sendEvent();
await sendEvent();
// Since none of the focused nodes handle this event, nothing should
// receive it.
expect(receivedAnEvent, isEmpty);
......@@ -500,13 +485,7 @@ void main() {
expect(lastMode, isNull);
focusManager.highlightStrategy = FocusHighlightStrategy.automatic;
expect(focusManager.highlightMode, equals(FocusHighlightMode.touch));
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
expect(callCount, equals(1));
expect(lastMode, FocusHighlightMode.traditional);
expect(focusManager.highlightMode, equals(FocusHighlightMode.traditional));
......
......@@ -2,20 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.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() {
testWidgets('Can dispose without keyboard', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
......@@ -40,21 +30,15 @@ void main() {
focusNode.requestFocus();
await tester.idle();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
await tester.idle();
expect(events.length, 1);
expect(events.length, 2);
expect(events[0].runtimeType, equals(RawKeyDownEvent));
expect(events[0].data.runtimeType, equals(RawKeyEventDataFuchsia));
final RawKeyEventDataFuchsia typedData = events[0].data;
expect(typedData.hidUsage, 0x04);
expect(typedData.codePoint, 0x64);
expect(typedData.hidUsage, 0x700e3);
expect(typedData.codePoint, 0x0);
expect(typedData.modifiers, RawKeyEventDataFuchsia.modifierLeftMeta);
expect(typedData.isModifierPressed(ModifierKey.metaModifier, side: KeyboardSide.left), isTrue);
......@@ -78,27 +62,15 @@ void main() {
focusNode.requestFocus();
await tester.idle();
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
await tester.idle();
expect(events.length, 1);
expect(events.length, 2);
events.clear();
await tester.pumpWidget(Container());
sendFakeKeyEvent(<String, dynamic>{
'type': 'keydown',
'keymap': 'fuchsia',
'hidUsage': 0x04,
'codePoint': 0x64,
'modifiers': RawKeyEventDataFuchsia.modifierLeftMeta,
});
await tester.sendKeyEvent(LogicalKeyboardKey.metaLeft, platform: 'fuchsia');
await tester.idle();
......
......@@ -6,18 +6,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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_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});
class TestAction extends CallbackAction {
......@@ -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() {
group(LogicalKeySet, () {
test('$LogicalKeySet passes parameters correctly.', () {
......@@ -248,7 +211,7 @@ void main() {
);
await tester.pump();
expect(Shortcuts.of(containerKey.currentContext), isNotNull);
testKeypress(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
});
......@@ -284,7 +247,7 @@ void main() {
);
await tester.pump();
expect(Shortcuts.of(containerKey.currentContext), isNotNull);
testKeypress(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
});
......
......@@ -51,6 +51,7 @@ export 'src/accessibility.dart';
export 'src/all_elements.dart';
export 'src/binding.dart';
export 'src/controller.dart';
export 'src/event_simulation.dart';
export 'src/finders.dart';
export 'src/goldens.dart';
export 'src/matchers.dart';
......
......@@ -796,6 +796,11 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
_pendingExceptionDetails = null;
_parentZone = null;
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,
'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 '
......
This diff is collapsed.
......@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
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 'binding.dart';
import 'controller.dart';
import 'event_simulation.dart';
import 'finders.dart';
import 'matchers.dart';
import 'test_async_utils.dart';
......@@ -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
/// 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