Unverified Commit e045ef29 authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

Keymap for Web (#41397)

parent a9c28d5f
...@@ -70,3 +70,20 @@ const Map<int, PhysicalKeyboardKey> kLinuxToPhysicalKey = <int, PhysicalKeyboard ...@@ -70,3 +70,20 @@ const Map<int, PhysicalKeyboardKey> kLinuxToPhysicalKey = <int, PhysicalKeyboard
@@@XKB_SCAN_CODE_MAP@@@ @@@XKB_SCAN_CODE_MAP@@@
}; };
/// Maps Web KeyboardEvent codes to the matching [LogicalKeyboardKey].
const Map<String, LogicalKeyboardKey> kWebToLogicalKey = <String, LogicalKeyboardKey>{
@@@WEB_LOGICAL_KEY_MAP@@@
};
/// Maps Web KeyboardEvent codes to the matching [PhysicalKeyboardKey].
const Map<String, PhysicalKeyboardKey> kWebToPhysicalKey = <String, PhysicalKeyboardKey>{
@@@WEB_PHYSICAL_KEY_MAP@@@
};
/// A map of Web KeyboardEvent codes which have printable representations, but appear
/// on the number pad. Used to provide different key objects for keys like
/// KEY_EQUALS and NUMPAD_EQUALS.
const Map<String, LogicalKeyboardKey> kWebNumPadMap = <String, LogicalKeyboardKey>{
@@@WEB_NUMPAD_MAP@@@
};
...@@ -108,6 +108,12 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -108,6 +108,12 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
return synonyms.toString(); return synonyms.toString();
} }
List<Key> get numpadKeyData {
return keyData.data.where((Key entry) {
return entry.constantName.startsWith('numpad') && entry.keyLabel != null;
}).toList();
}
/// This generates the map of USB HID codes to physical keys. /// This generates the map of USB HID codes to physical keys.
String get predefinedHidCodeMap { String get predefinedHidCodeMap {
final StringBuffer scanCodeMap = StringBuffer(); final StringBuffer scanCodeMap = StringBuffer();
...@@ -139,10 +145,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -139,10 +145,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
/// This generates the map of GLFW number pad key codes to logical keys. /// This generates the map of GLFW number pad key codes to logical keys.
String get glfwNumpadMap { String get glfwNumpadMap {
final StringBuffer glfwNumpadMap = StringBuffer(); final StringBuffer glfwNumpadMap = StringBuffer();
final List<Key> onlyNumpads = keyData.data.where((Key entry) { for (Key entry in numpadKeyData) {
return entry.constantName.startsWith('numpad') && entry.keyLabel != null;
}).toList();
for (Key entry in onlyNumpads) {
if (entry.glfwKeyCodes != null) { if (entry.glfwKeyCodes != null) {
for (int code in entry.glfwKeyCodes.cast<int>()) { for (int code in entry.glfwKeyCodes.cast<int>()) {
glfwNumpadMap.writeln(' $code: LogicalKeyboardKey.${entry.constantName},'); glfwNumpadMap.writeln(' $code: LogicalKeyboardKey.${entry.constantName},');
...@@ -192,10 +195,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -192,10 +195,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
/// This generates the map of Android number pad key codes to logical keys. /// This generates the map of Android number pad key codes to logical keys.
String get androidNumpadMap { String get androidNumpadMap {
final StringBuffer androidKeyCodeMap = StringBuffer(); final StringBuffer androidKeyCodeMap = StringBuffer();
final List<Key> onlyNumpads = keyData.data.where((Key entry) { for (Key entry in numpadKeyData) {
return entry.constantName.startsWith('numpad') && entry.keyLabel != null;
}).toList();
for (Key entry in onlyNumpads) {
if (entry.androidKeyCodes != null) { if (entry.androidKeyCodes != null) {
for (int code in entry.androidKeyCodes.cast<int>()) { for (int code in entry.androidKeyCodes.cast<int>()) {
androidKeyCodeMap.writeln(' $code: LogicalKeyboardKey.${entry.constantName},'); androidKeyCodeMap.writeln(' $code: LogicalKeyboardKey.${entry.constantName},');
...@@ -232,10 +232,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -232,10 +232,7 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
/// This generates the map of macOS number pad key codes to logical keys. /// This generates the map of macOS number pad key codes to logical keys.
String get macOsNumpadMap { String get macOsNumpadMap {
final StringBuffer macOsNumPadMap = StringBuffer(); final StringBuffer macOsNumPadMap = StringBuffer();
final List<Key> onlyNumpads = keyData.data.where((Key entry) { for (Key entry in numpadKeyData) {
return entry.constantName.startsWith('numpad') && entry.keyLabel != null;
}).toList();
for (Key entry in onlyNumpads) {
if (entry.macOsScanCode != null) { if (entry.macOsScanCode != null) {
macOsNumPadMap.writeln(' ${toHex(entry.macOsScanCode)}: LogicalKeyboardKey.${entry.constantName},'); macOsNumPadMap.writeln(' ${toHex(entry.macOsScanCode)}: LogicalKeyboardKey.${entry.constantName},');
} }
...@@ -265,6 +262,39 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -265,6 +262,39 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
return fuchsiaScanCodeMap.toString().trimRight(); return fuchsiaScanCodeMap.toString().trimRight();
} }
/// This generates the map of Web KeyboardEvent codes to logical keys.
String get webLogicalKeyMap {
final StringBuffer result = StringBuffer();
for (Key entry in keyData.data) {
if (entry.name != null) {
result.writeln(" '${entry.name}': LogicalKeyboardKey.${entry.constantName},");
}
}
return result.toString().trimRight();
}
/// This generates the map of Web KeyboardEvent codes to physical keys.
String get webPhysicalKeyMap {
final StringBuffer result = StringBuffer();
for (Key entry in keyData.data) {
if (entry.name != null) {
result.writeln(" '${entry.name}': PhysicalKeyboardKey.${entry.constantName},");
}
}
return result.toString().trimRight();
}
/// This generates the map of Web number pad codes to logical keys.
String get webNumpadMap {
final StringBuffer result = StringBuffer();
for (Key entry in numpadKeyData) {
if (entry.name != null) {
result.writeln(" '${entry.name}': LogicalKeyboardKey.${entry.constantName},");
}
}
return result.toString().trimRight();
}
/// Substitutes the various maps and definitions into the template file for /// Substitutes the various maps and definitions into the template file for
/// keyboard_key.dart. /// keyboard_key.dart.
String generateKeyboardKeys() { String generateKeyboardKeys() {
...@@ -297,6 +327,9 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK ...@@ -297,6 +327,9 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
'GLFW_KEY_CODE_MAP': glfwKeyCodeMap, 'GLFW_KEY_CODE_MAP': glfwKeyCodeMap,
'GLFW_NUMPAD_MAP': glfwNumpadMap, 'GLFW_NUMPAD_MAP': glfwNumpadMap,
'XKB_SCAN_CODE_MAP': xkbScanCodeMap, 'XKB_SCAN_CODE_MAP': xkbScanCodeMap,
'WEB_LOGICAL_KEY_MAP': webLogicalKeyMap,
'WEB_PHYSICAL_KEY_MAP': webPhysicalKeyMap,
'WEB_NUMPAD_MAP': webNumpadMap,
}; };
final String template = File(path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data', 'keyboard_maps.tmpl')).readAsStringSync(); final String template = File(path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data', 'keyboard_maps.tmpl')).readAsStringSync();
......
...@@ -28,6 +28,7 @@ export 'src/services/raw_keyboard_android.dart'; ...@@ -28,6 +28,7 @@ export 'src/services/raw_keyboard_android.dart';
export 'src/services/raw_keyboard_fuchsia.dart'; export 'src/services/raw_keyboard_fuchsia.dart';
export 'src/services/raw_keyboard_linux.dart'; export 'src/services/raw_keyboard_linux.dart';
export 'src/services/raw_keyboard_macos.dart'; export 'src/services/raw_keyboard_macos.dart';
export 'src/services/raw_keyboard_web.dart';
export 'src/services/system_channels.dart'; export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart'; export 'src/services/system_navigator.dart';
......
...@@ -11,6 +11,7 @@ import 'raw_keyboard_android.dart'; ...@@ -11,6 +11,7 @@ import 'raw_keyboard_android.dart';
import 'raw_keyboard_fuchsia.dart'; import 'raw_keyboard_fuchsia.dart';
import 'raw_keyboard_linux.dart'; import 'raw_keyboard_linux.dart';
import 'raw_keyboard_macos.dart'; import 'raw_keyboard_macos.dart';
import 'raw_keyboard_web.dart';
import 'system_channels.dart'; import 'system_channels.dart';
/// An enum describing the side of the keyboard that a key is on, to allow /// An enum describing the side of the keyboard that a key is on, to allow
...@@ -284,6 +285,13 @@ abstract class RawKeyEvent extends Diagnosticable { ...@@ -284,6 +285,13 @@ abstract class RawKeyEvent extends Diagnosticable {
scanCode: message['scanCode'] ?? 0, scanCode: message['scanCode'] ?? 0,
modifiers: message['modifiers'] ?? 0); modifiers: message['modifiers'] ?? 0);
break; break;
case 'web':
data = RawKeyEventDataWeb(
code: message['code'],
key: message['key'],
metaState: message['metaState'],
);
break;
default: default:
// We don't yet implement raw key events on iOS or other platforms, but // We don't yet implement raw key events on iOS or other platforms, but
// we don't hit this exception because the engine never sends us these // we don't hit this exception because the engine never sends us these
......
// 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/foundation.dart';
import 'keyboard_key.dart';
import 'keyboard_maps.dart';
import 'raw_keyboard.dart';
/// Platform-specific key event data for Web.
///
/// See also:
///
/// * [RawKeyboard], which uses this interface to expose key data.
@immutable
class RawKeyEventDataWeb extends RawKeyEventData {
/// Creates a key event data structure specific for Web.
///
/// The [keyCode] and [metaState] arguments must not be null.
const RawKeyEventDataWeb({
@required this.code,
@required this.key,
this.metaState = modifierNone,
}) : assert(code != null),
assert(metaState != null);
/// The `KeyboardEvent.code` corresponding to this event.
///
/// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code>
/// for more information.
final String code;
/// The `KeyboardEvent.key` corresponding to this event.
///
/// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key>
/// for more information.
final String key;
/// The modifiers that were present when the key event occurred.
///
/// See `lib/src/engine/keyboard.dart` in the web engine for the numerical
/// values of the `metaState`. These constants are also replicated as static
/// constants in this class.
///
/// See also:
///
/// * [modifiersPressed], which returns a Map of currently pressed modifiers
/// and their keyboard side.
/// * [isModifierPressed], to see if a specific modifier is pressed.
/// * [isControlPressed], to see if a CTRL key is pressed.
/// * [isShiftPressed], to see if a SHIFT key is pressed.
/// * [isAltPressed], to see if an ALT key is pressed.
/// * [isMetaPressed], to see if a META key is pressed.
final int metaState;
@override
String get keyLabel => key;
@override
PhysicalKeyboardKey get physicalKey {
return kWebToPhysicalKey[code] ?? PhysicalKeyboardKey.none;
}
@override
LogicalKeyboardKey get logicalKey {
// Look to see if the keyCode is a printable number pad key, so that a
// difference between regular keys (e.g. ".") and the number pad version
// (e.g. the "." on the number pad) can be determined.
final LogicalKeyboardKey numPadKey = kWebNumPadMap[code];
if (numPadKey != null) {
return numPadKey;
}
// Look to see if the [code] is one we know about and have a mapping for.
final LogicalKeyboardKey newKey = kWebToLogicalKey[code];
if (newKey != null) {
return newKey;
}
// This is a non-printable key that we don't know about, so we mint a new
// code with the autogenerated bit set.
const int webKeyIdPlane = 0x00800000000;
return LogicalKeyboardKey(
webKeyIdPlane | code.hashCode | LogicalKeyboardKey.autogeneratedMask,
debugName: kReleaseMode ? null : 'Unknown Web code "$code"',
);
}
@override
bool isModifierPressed(
ModifierKey key, {
KeyboardSide side = KeyboardSide.any,
}) {
switch (key) {
case ModifierKey.controlModifier:
return metaState & modifierControl != 0;
case ModifierKey.shiftModifier:
return metaState & modifierShift != 0;
case ModifierKey.altModifier:
return metaState & modifierAlt != 0;
case ModifierKey.metaModifier:
return metaState & modifierMeta != 0;
case ModifierKey.numLockModifier:
return metaState & modifierNumLock != 0;
case ModifierKey.capsLockModifier:
return metaState & modifierCapsLock != 0;
case ModifierKey.scrollLockModifier:
return metaState & modifierScrollLock != 0;
case ModifierKey.functionModifier:
case ModifierKey.symbolModifier:
default:
// On Web, the browser doesn't report the state of the FN and SYM modifiers.
return false;
}
}
@override
KeyboardSide getModifierSide(ModifierKey key) {
// On Web, we don't distinguish the sides of modifier keys. Both left shift
// and right shift, for example, are reported as the "Shift" modifier.
//
// See <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState>
// for more information.
return KeyboardSide.all;
}
// Modifier key masks.
/// No modifier keys are pressed in the [metaState] field.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierNone = 0;
/// This mask is used to check the [metaState] field to test whether one of
/// the SHIFT modifier keys is pressed.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierShift = 0x01;
/// This mask is used to check the [metaState] field to test whether one of
/// the ALT modifier keys is pressed.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierAlt = 0x02;
/// This mask is used to check the [metaState] field to test whether one of
/// the CTRL modifier keys is pressed.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierControl = 0x04;
/// This mask is used to check the [metaState] field to test whether one of
/// the META modifier keys is pressed.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierMeta = 0x08;
/// This mask is used to check the [metaState] field to test whether the NUM
/// LOCK modifier key is on.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierNumLock = 0x10;
/// This mask is used to check the [metaState] field to test whether the CAPS
/// LOCK modifier key is on.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierCapsLock = 0x20;
/// This mask is used to check the [metaState] field to test whether the
/// SCROLL LOCK modifier key is on.
///
/// Use this value if you need to decode the [metaState] field yourself, but
/// it's much easier to use [isModifierPressed] if you just want to know if
/// a modifier is pressed.
static const int modifierScrollLock = 0x40;
@override
String toString() {
return '$runtimeType(keyLabel: $keyLabel, code: $code, '
'metaState: $metaState, modifiers down: $modifiersPressed)';
}
}
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// 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.
@TestOn('!chrome') // web does not have keyboard support yet.
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -321,7 +319,7 @@ void main() { ...@@ -321,7 +319,7 @@ void main() {
expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft)); expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
expect(data.keyLabel, isNull); expect(data.keyLabel, isNull);
}); });
}); }, skip: isBrowser);
group('RawKeyEventDataMacOs', () { group('RawKeyEventDataMacOs', () {
const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{ const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
RawKeyEventDataMacOs.modifierOption | RawKeyEventDataMacOs.modifierLeftOption: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.left), RawKeyEventDataMacOs.modifierOption | RawKeyEventDataMacOs.modifierLeftOption: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.left),
...@@ -465,8 +463,7 @@ void main() { ...@@ -465,8 +463,7 @@ void main() {
expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft)); expect(data.logicalKey, equals(LogicalKeyboardKey.arrowLeft));
expect(data.logicalKey.keyLabel, isNull); expect(data.logicalKey.keyLabel, isNull);
}); });
}, skip: isBrowser);
});
group('RawKeyEventDataLinux-GFLW', () { group('RawKeyEventDataLinux-GFLW', () {
const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{ const Map<int, _ModifierCheck> modifierTests = <int, _ModifierCheck>{
GLFWKeyHelper.modifierAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.any), GLFWKeyHelper.modifierAlt: _ModifierCheck(ModifierKey.altModifier, KeyboardSide.any),
...@@ -622,5 +619,130 @@ void main() { ...@@ -622,5 +619,130 @@ void main() {
expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft)); expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
expect(data.keyLabel, isNull); expect(data.keyLabel, isNull);
}); });
}, skip: isBrowser);
group('RawKeyEventDataWeb', () {
const Map<int, ModifierKey> modifierTests = <int, ModifierKey>{
RawKeyEventDataWeb.modifierAlt: ModifierKey.altModifier,
RawKeyEventDataWeb.modifierShift: ModifierKey.shiftModifier,
RawKeyEventDataWeb.modifierControl: ModifierKey.controlModifier,
RawKeyEventDataWeb.modifierMeta: ModifierKey.metaModifier,
RawKeyEventDataWeb.modifierCapsLock: ModifierKey.capsLockModifier,
RawKeyEventDataWeb.modifierNumLock: ModifierKey.numLockModifier,
RawKeyEventDataWeb.modifierScrollLock: ModifierKey.scrollLockModifier,
};
test('modifier keys are recognized individually', () {
for (int modifier in modifierTests.keys) {
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'RandomCode',
'key': null,
'metaState': modifier,
});
final RawKeyEventDataWeb data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier] == key) {
expect(
data.isModifierPressed(key),
isTrue,
reason: "$key should be pressed with metaState $modifier, but isn't.",
);
} else {
expect(
data.isModifierPressed(key),
isFalse,
reason: '$key should not be pressed with metaState $modifier.',
);
}
}
}
});
test('modifier keys are recognized when combined', () {
for (int modifier in modifierTests.keys) {
if (modifier == RawKeyEventDataWeb.modifierMeta) {
// No need to combine meta key with itself.
continue;
}
final RawKeyEvent event = RawKeyEvent.fromMessage(<String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'RandomCode',
'key': null,
'metaState': modifier | RawKeyEventDataWeb.modifierMeta,
});
final RawKeyEventDataWeb data = event.data;
for (ModifierKey key in ModifierKey.values) {
if (modifierTests[modifier] == key || key == ModifierKey.metaModifier) {
expect(
data.isModifierPressed(key),
isTrue,
reason: '$key should be pressed with metaState $modifier '
"and additional key ${RawKeyEventDataWeb.modifierMeta}, but isn't.",
);
} else {
expect(
data.isModifierPressed(key),
isFalse,
reason: '$key should not be pressed with metaState $modifier '
'and additional key ${RawKeyEventDataWeb.modifierMeta}.',
);
}
}
}
});
test('Printable keyboard keys are correctly translated', () {
final RawKeyEvent keyAEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'KeyA',
'key': 'a',
'metaState': 0x0,
});
final RawKeyEventDataWeb data = keyAEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.keyA));
expect(data.logicalKey, equals(LogicalKeyboardKey.keyA));
expect(data.keyLabel, equals('a'));
});
test('Control keyboard keys are correctly translated', () {
final RawKeyEvent escapeKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'Escape',
'key': null,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = escapeKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.escape));
expect(data.logicalKey, equals(LogicalKeyboardKey.escape));
expect(data.keyLabel, isNull);
});
test('Modifier keyboard keys are correctly translated', () {
final RawKeyEvent shiftKeyEvent = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'ShiftLeft',
'keyLabel': null,
'metaState': RawKeyEventDataWeb.modifierShift,
});
final RawKeyEventDataWeb data = shiftKeyEvent.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.shiftLeft));
expect(data.logicalKey, equals(LogicalKeyboardKey.shiftLeft));
expect(data.keyLabel, isNull);
});
test('Arrow keys from a keyboard give correct physical key mappings', () {
final RawKeyEvent arrowKeyDown = RawKeyEvent.fromMessage(const <String, dynamic>{
'type': 'keydown',
'keymap': 'web',
'code': 'ArrowDown',
'key': null,
'metaState': 0x0,
});
final RawKeyEventDataWeb data = arrowKeyDown.data;
expect(data.physicalKey, equals(PhysicalKeyboardKey.arrowDown));
expect(data.logicalKey, equals(LogicalKeyboardKey.arrowDown));
expect(data.keyLabel, isNull);
});
}); });
} }
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