|
| 1 | +// Copyright 2013 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'dart:typed_data'; |
| 6 | +import 'package:meta/meta.dart' show isTest; |
| 7 | +import 'package:quiver/testing/async.dart'; |
| 8 | +import 'package:test/bootstrap/browser.dart'; |
| 9 | +import 'package:test/test.dart'; |
| 10 | +import 'package:ui/src/engine.dart'; |
| 11 | +import 'package:ui/ui.dart' as ui; |
| 12 | + |
| 13 | +const int kLocationStandard = 0; |
| 14 | +const int kLocationLeft = 1; |
| 15 | +const int kLocationRight = 2; |
| 16 | +const int kLocationNumpad = 3; |
| 17 | + |
| 18 | +final int kPhysicalKeyA = kWebToPhysicalKey['KeyA']!; |
| 19 | +final int kPhysicalKeyE = kWebToPhysicalKey['KeyE']!; |
| 20 | +final int kPhysicalKeyU = kWebToPhysicalKey['KeyU']!; |
| 21 | +final int kPhysicalDigit1 = kWebToPhysicalKey['Digit1']!; |
| 22 | +final int kPhysicalNumpad1 = kWebToPhysicalKey['Numpad1']!; |
| 23 | +final int kPhysicalShiftLeft = kWebToPhysicalKey['ShiftLeft']!; |
| 24 | +final int kPhysicalShiftRight = kWebToPhysicalKey['ShiftRight']!; |
| 25 | +final int kPhysicalMetaLeft = kWebToPhysicalKey['MetaLeft']!; |
| 26 | +final int kPhysicalTab = kWebToPhysicalKey['Tab']!; |
| 27 | +final int kPhysicalCapsLock = kWebToPhysicalKey['CapsLock']!; |
| 28 | +final int kPhysicalScrollLock = kWebToPhysicalKey['ScrollLock']!; |
| 29 | +// A web-specific physical key when code is empty. |
| 30 | +const int kPhysicalEmptyCode = 0x1700000000; |
| 31 | + |
| 32 | +const int kLogicalKeyA = 0x00000000061; |
| 33 | +const int kLogicalKeyU = 0x00000000075; |
| 34 | +const int kLogicalDigit1 = 0x00000000031; |
| 35 | +final int kLogicalNumpad1 = kWebLogicalLocationMap['1']![kLocationNumpad]!; |
| 36 | +final int kLogicalShiftLeft = kWebLogicalLocationMap['Shift']![kLocationLeft]!; |
| 37 | +final int kLogicalShiftRight = kWebLogicalLocationMap['Shift']![kLocationRight]!; |
| 38 | +final int kLogicalCtrlLeft = kWebLogicalLocationMap['Control']![kLocationLeft]!; |
| 39 | +final int kLogicalAltLeft = kWebLogicalLocationMap['Alt']![kLocationLeft]!; |
| 40 | +final int kLogicalMetaLeft = kWebLogicalLocationMap['Meta']![kLocationLeft]!; |
| 41 | +const int kLogicalTab = 0x0000000009; |
| 42 | +final int kLogicalCapsLock = kWebToLogicalKey['CapsLock']!; |
| 43 | +final int kLogicalScrollLock = kWebToLogicalKey['ScrollLock']!; |
| 44 | + |
| 45 | +typedef VoidCallback = void Function(); |
| 46 | + |
| 47 | +void main() { |
| 48 | + internalBootstrapBrowserTest(() => testMain); |
| 49 | +} |
| 50 | + |
| 51 | +ui.PlatformMessageCallback storeChannelDataTo(List<Map<String, dynamic>> storage) { |
| 52 | + return (String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { |
| 53 | + expect(channel, 'flutter/keyevent'); |
| 54 | + final Map<String, dynamic>? dataReceived = |
| 55 | + const JSONMessageCodec().decodeMessage(data) as Map<String, dynamic>?; |
| 56 | + expect(dataReceived, isNotNull); |
| 57 | + storage.add(dataReceived!); |
| 58 | + }; |
| 59 | +} |
| 60 | + |
| 61 | +void testMain() { |
| 62 | + /// Used to save and restore [ui.window.onPlatformMessage] after each test. |
| 63 | + ui.PlatformMessageCallback? savedCallback; |
| 64 | + |
| 65 | + setUp(() { |
| 66 | + savedCallback = ui.window.onPlatformMessage; |
| 67 | + }); |
| 68 | + |
| 69 | + tearDown(() { |
| 70 | + ui.window.onPlatformMessage = savedCallback; |
| 71 | + }); |
| 72 | + |
| 73 | + test('Single key press, repeat, and release', () { |
| 74 | + final List<ui.KeyData> keyDataList = <ui.KeyData>[]; |
| 75 | + final List<Map<String, dynamic>> channelData = <Map<String, dynamic>>[]; |
| 76 | + ui.window.onPlatformMessage = storeChannelDataTo(channelData); |
| 77 | + final KeyboardBinding binding = KeyboardBinding.instance!; |
| 78 | + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { |
| 79 | + keyDataList.add(key); |
| 80 | + // Only handle down events |
| 81 | + return key.type == ui.KeyEventType.down; |
| 82 | + }); |
| 83 | + bool preventedDefault = false; |
| 84 | + void onPreventDefault() { preventedDefault = true; } |
| 85 | + |
| 86 | + converter.handleEvent(keyDownEvent('KeyA', 'a') |
| 87 | + ..timeStamp = 1 |
| 88 | + ..onPreventDefault = onPreventDefault |
| 89 | + ); |
| 90 | + expectKeyData(keyDataList.last, |
| 91 | + timeStamp: const Duration(milliseconds: 1), |
| 92 | + type: ui.KeyEventType.down, |
| 93 | + physical: kPhysicalKeyA, |
| 94 | + logical: kLogicalKeyA, |
| 95 | + character: 'a', |
| 96 | + ); |
| 97 | + expect(channelData, hasLength(1)); |
| 98 | + expect(channelData.last, <String, dynamic>{ |
| 99 | + 'type': 'keyup', |
| 100 | + 'keymap': 'web', |
| 101 | + 'code': 'KeyA', |
| 102 | + 'location': 0, |
| 103 | + 'key': 'a', |
| 104 | + 'metaState': 0x0, |
| 105 | + 'keyCode': 1, |
| 106 | + }); |
| 107 | + expect(preventedDefault, isTrue); |
| 108 | + preventedDefault = false; |
| 109 | + |
| 110 | + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') |
| 111 | + ..timeStamp = 1.5 |
| 112 | + ..onPreventDefault = onPreventDefault |
| 113 | + ); |
| 114 | + expectKeyData(keyDataList.last, |
| 115 | + timeStamp: const Duration(milliseconds: 1, microseconds: 500), |
| 116 | + type: ui.KeyEventType.repeat, |
| 117 | + physical: kPhysicalKeyA, |
| 118 | + logical: kLogicalKeyA, |
| 119 | + character: 'a', |
| 120 | + ); |
| 121 | + expect(preventedDefault, isFalse); |
| 122 | + |
| 123 | + converter.handleEvent(keyRepeatedDownEvent('KeyA', 'a') |
| 124 | + ..timeStamp = 1500 |
| 125 | + ..onPreventDefault = onPreventDefault |
| 126 | + ); |
| 127 | + expectKeyData(keyDataList.last, |
| 128 | + timeStamp: const Duration(seconds: 1, milliseconds: 500), |
| 129 | + type: ui.KeyEventType.repeat, |
| 130 | + physical: kPhysicalKeyA, |
| 131 | + logical: kLogicalKeyA, |
| 132 | + character: 'a', |
| 133 | + ); |
| 134 | + expect(preventedDefault, isFalse); |
| 135 | + |
| 136 | + converter.handleEvent(keyUpEvent('KeyA', 'a') |
| 137 | + ..timeStamp = 2000.5 |
| 138 | + ..onPreventDefault = onPreventDefault |
| 139 | + ); |
| 140 | + expectKeyData(keyDataList.last, |
| 141 | + timeStamp: const Duration(seconds: 2, microseconds: 500), |
| 142 | + type: ui.KeyEventType.up, |
| 143 | + physical: kPhysicalKeyA, |
| 144 | + logical: kLogicalKeyA, |
| 145 | + character: null, |
| 146 | + ); |
| 147 | + expect(preventedDefault, isFalse); |
| 148 | + }); |
| 149 | +} |
| 150 | + |
| 151 | +class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { |
| 152 | + MockKeyboardEvent({ |
| 153 | + required this.type, |
| 154 | + required this.code, |
| 155 | + required this.key, |
| 156 | + this.timeStamp = 0, |
| 157 | + this.repeat = false, |
| 158 | + this.altKey = false, |
| 159 | + this.ctrlKey = false, |
| 160 | + this.shiftKey = false, |
| 161 | + this.metaKey = false, |
| 162 | + this.location = 0, |
| 163 | + this.onPreventDefault, |
| 164 | + }); |
| 165 | + |
| 166 | + @override |
| 167 | + String type; |
| 168 | + |
| 169 | + @override |
| 170 | + String? code; |
| 171 | + |
| 172 | + @override |
| 173 | + String? key; |
| 174 | + |
| 175 | + @override |
| 176 | + bool? repeat; |
| 177 | + |
| 178 | + @override |
| 179 | + num? timeStamp; |
| 180 | + |
| 181 | + @override |
| 182 | + bool altKey; |
| 183 | + |
| 184 | + @override |
| 185 | + bool ctrlKey; |
| 186 | + |
| 187 | + @override |
| 188 | + bool shiftKey; |
| 189 | + |
| 190 | + @override |
| 191 | + bool metaKey; |
| 192 | + |
| 193 | + @override |
| 194 | + int? location; |
| 195 | + |
| 196 | + @override |
| 197 | + bool getModifierState(String key) => modifierState.contains(key); |
| 198 | + final Set<String> modifierState = <String>{}; |
| 199 | + |
| 200 | + @override |
| 201 | + void preventDefault() { onPreventDefault?.call(); } |
| 202 | + VoidCallback? onPreventDefault; |
| 203 | +} |
| 204 | + |
| 205 | +// Flags used for the `modifiers` argument of `key***Event` functions. |
| 206 | +const int kAlt = 0x1; |
| 207 | +const int kCtrl = 0x2; |
| 208 | +const int kShift = 0x4; |
| 209 | +const int kMeta = 0x8; |
| 210 | + |
| 211 | +// Utility functions to make code more concise. |
| 212 | +// |
| 213 | +// To add timeStamp or onPreventDefault, use syntax like `..timeStamp = `. |
| 214 | +MockKeyboardEvent keyDownEvent(String code, String key, [int modifiers = 0, int location = 0]) { |
| 215 | + return MockKeyboardEvent( |
| 216 | + type: 'keydown', |
| 217 | + code: code, |
| 218 | + key: key, |
| 219 | + altKey: modifiers & kAlt != 0, |
| 220 | + ctrlKey: modifiers & kCtrl != 0, |
| 221 | + shiftKey: modifiers & kShift != 0, |
| 222 | + metaKey: modifiers & kMeta != 0, |
| 223 | + location: location, |
| 224 | + ); |
| 225 | +} |
| 226 | + |
| 227 | +MockKeyboardEvent keyUpEvent(String code, String key, [int modifiers = 0, int location = 0]) { |
| 228 | + return MockKeyboardEvent( |
| 229 | + type: 'keyup', |
| 230 | + code: code, |
| 231 | + key: key, |
| 232 | + altKey: modifiers & kAlt != 0, |
| 233 | + ctrlKey: modifiers & kCtrl != 0, |
| 234 | + shiftKey: modifiers & kShift != 0, |
| 235 | + metaKey: modifiers & kMeta != 0, |
| 236 | + location: location, |
| 237 | + ); |
| 238 | +} |
| 239 | + |
| 240 | +MockKeyboardEvent keyRepeatedDownEvent(String code, String key, [int modifiers = 0, int location = 0]) { |
| 241 | + return MockKeyboardEvent( |
| 242 | + type: 'keydown', |
| 243 | + code: code, |
| 244 | + key: key, |
| 245 | + altKey: modifiers & kAlt != 0, |
| 246 | + ctrlKey: modifiers & kCtrl != 0, |
| 247 | + shiftKey: modifiers & kShift != 0, |
| 248 | + metaKey: modifiers & kMeta != 0, |
| 249 | + repeat: true, |
| 250 | + location: location, |
| 251 | + ); |
| 252 | +} |
| 253 | + |
| 254 | +// Flags used for the `activeLocks` argument of expectKeyData. |
| 255 | +const int kCapsLock = 0x1; |
| 256 | +const int kNumlLock = 0x2; |
| 257 | +const int kScrollLock = 0x4; |
| 258 | + |
| 259 | +void expectKeyData( |
| 260 | + ui.KeyData target, { |
| 261 | + required ui.KeyEventType type, |
| 262 | + required int physical, |
| 263 | + required int logical, |
| 264 | + required String? character, |
| 265 | + Duration? timeStamp, |
| 266 | + bool synthesized = false, |
| 267 | +}) { |
| 268 | + expect(target.type, type); |
| 269 | + expect(target.physical, physical); |
| 270 | + expect(target.logical, logical); |
| 271 | + expect(target.character, character); |
| 272 | + expect(target.synthesized, synthesized); |
| 273 | + if (timeStamp != null) |
| 274 | + expect(target.timeStamp, equals(timeStamp)); |
| 275 | +} |
| 276 | + |
| 277 | +typedef FakeAsyncTest = void Function(FakeAsync); |
| 278 | + |
| 279 | +@isTest |
| 280 | +void testFakeAsync(String description, FakeAsyncTest fn) { |
| 281 | + test(description, () { |
| 282 | + FakeAsync().run(fn); |
| 283 | + }); |
| 284 | +} |
0 commit comments